mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-09 06:39:25 +00:00
3425 lines
112 KiB
Rust
3425 lines
112 KiB
Rust
use crate::disk::FileInfoVersions;
|
|
use crate::file_meta_inline::InlineData;
|
|
use crate::store_api::RawFileInfo;
|
|
use crate::store_err::StorageError;
|
|
use crate::{
|
|
disk::error::DiskError,
|
|
store_api::{ErasureInfo, FileInfo, ObjectPartInfo, ERASURE_ALGORITHM},
|
|
};
|
|
use byteorder::ByteOrder;
|
|
use common::error::{Error, Result};
|
|
use rmp::Marker;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::cmp::Ordering;
|
|
use std::fmt::Display;
|
|
use std::io::{self, Read, Write};
|
|
use std::{collections::HashMap, io::Cursor};
|
|
use time::OffsetDateTime;
|
|
use tokio::io::AsyncRead;
|
|
use tracing::{error, warn};
|
|
use uuid::Uuid;
|
|
use xxhash_rust::xxh64;
|
|
|
|
// XL header specifies the format
|
|
pub static XL_FILE_HEADER: [u8; 4] = [b'X', b'L', b'2', b' '];
|
|
// pub static XL_FILE_VERSION_CURRENT: [u8; 4] = [0; 4];
|
|
|
|
// Current version being written.
|
|
// static XL_FILE_VERSION: [u8; 4] = [1, 0, 3, 0];
|
|
static XL_FILE_VERSION_MAJOR: u16 = 1;
|
|
static XL_FILE_VERSION_MINOR: u16 = 3;
|
|
static XL_HEADER_VERSION: u8 = 3;
|
|
static XL_META_VERSION: u8 = 2;
|
|
static XXHASH_SEED: u64 = 0;
|
|
|
|
const XL_FLAG_FREE_VERSION: u8 = 1 << 0;
|
|
// const XL_FLAG_USES_DATA_DIR: u8 = 1 << 1;
|
|
const _XL_FLAG_INLINE_DATA: u8 = 1 << 2;
|
|
|
|
const META_DATA_READ_DEFAULT: usize = 4 << 10;
|
|
const MSGP_UINT32_SIZE: usize = 5;
|
|
|
|
// type ScanHeaderVersionFn = Box<dyn Fn(usize, &[u8], &[u8]) -> Result<()>>;
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct FileMeta {
|
|
pub versions: Vec<FileMetaShallowVersion>,
|
|
pub data: InlineData, // TODO: xlMetaInlineData
|
|
pub meta_ver: u8,
|
|
}
|
|
|
|
impl FileMeta {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
meta_ver: XL_META_VERSION,
|
|
data: InlineData::new(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
// isXL2V1Format
|
|
#[tracing::instrument(level = "debug", skip_all)]
|
|
pub fn is_xl2_v1_format(buf: &[u8]) -> bool {
|
|
!matches!(Self::check_xl2_v1(buf), Err(_e))
|
|
}
|
|
|
|
#[tracing::instrument(level = "debug", skip_all)]
|
|
pub fn load(buf: &[u8]) -> Result<FileMeta> {
|
|
let mut xl = FileMeta::default();
|
|
xl.unmarshal_msg(buf)?;
|
|
|
|
Ok(xl)
|
|
}
|
|
|
|
// check_xl2_v1 读 xl 文件头,返回后续内容,版本信息
|
|
// checkXL2V1
|
|
#[tracing::instrument(level = "debug", skip_all)]
|
|
pub fn check_xl2_v1(buf: &[u8]) -> Result<(&[u8], u16, u16)> {
|
|
if buf.len() < 8 {
|
|
return Err(Error::msg("xl file header not exists"));
|
|
}
|
|
|
|
if buf[0..4] != XL_FILE_HEADER {
|
|
return Err(Error::msg("xl file header err"));
|
|
}
|
|
|
|
let major = byteorder::LittleEndian::read_u16(&buf[4..6]);
|
|
let minor = byteorder::LittleEndian::read_u16(&buf[6..8]);
|
|
if major > XL_FILE_VERSION_MAJOR {
|
|
return Err(Error::msg("xl file version err"));
|
|
}
|
|
|
|
Ok((&buf[8..], major, minor))
|
|
}
|
|
|
|
// 固定 u32
|
|
pub fn read_bytes_header(buf: &[u8]) -> Result<(u32, &[u8])> {
|
|
if buf.len() < 5 {
|
|
return Err(Error::new(io::Error::new(
|
|
io::ErrorKind::UnexpectedEof,
|
|
format!("Buffer too small: {} bytes, need at least 5", buf.len()),
|
|
)));
|
|
}
|
|
|
|
let (mut size_buf, _) = buf.split_at(5);
|
|
|
|
// 取 meta 数据,buf = crc + data
|
|
let bin_len = rmp::decode::read_bin_len(&mut size_buf)?;
|
|
|
|
Ok((bin_len, &buf[5..]))
|
|
}
|
|
|
|
pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
|
|
let i = buf.len() as u64;
|
|
|
|
// check version, buf = buf[8..]
|
|
let (buf, _, _) = Self::check_xl2_v1(buf)?;
|
|
|
|
let (mut size_buf, buf) = buf.split_at(5);
|
|
|
|
// 取 meta 数据,buf = crc + data
|
|
let bin_len = rmp::decode::read_bin_len(&mut size_buf)?;
|
|
|
|
let (meta, buf) = buf.split_at(bin_len as usize);
|
|
|
|
let (mut crc_buf, buf) = buf.split_at(5);
|
|
|
|
// crc check
|
|
let crc = rmp::decode::read_u32(&mut crc_buf)?;
|
|
let meta_crc = xxh64::xxh64(meta, XXHASH_SEED) as u32;
|
|
|
|
if crc != meta_crc {
|
|
return Err(Error::msg("xl file crc check failed"));
|
|
}
|
|
|
|
if !buf.is_empty() {
|
|
self.data.update(buf);
|
|
self.data.validate()?;
|
|
}
|
|
|
|
// 解析 meta
|
|
if !meta.is_empty() {
|
|
let (versions_len, _, meta_ver, meta) = Self::decode_xl_headers(meta)?;
|
|
|
|
// let (_, meta) = meta.split_at(read_size as usize);
|
|
|
|
self.meta_ver = meta_ver;
|
|
|
|
self.versions = Vec::with_capacity(versions_len);
|
|
|
|
let mut cur: Cursor<&[u8]> = Cursor::new(meta);
|
|
for _ in 0..versions_len {
|
|
let bin_len = rmp::decode::read_bin_len(&mut cur)? as usize;
|
|
let start = cur.position() as usize;
|
|
let end = start + bin_len;
|
|
let header_buf = &meta[start..end];
|
|
|
|
let mut ver = FileMetaShallowVersion::default();
|
|
ver.header.unmarshal_msg(header_buf)?;
|
|
|
|
cur.set_position(end as u64);
|
|
|
|
let bin_len = rmp::decode::read_bin_len(&mut cur)? as usize;
|
|
let start = cur.position() as usize;
|
|
let end = start + bin_len;
|
|
let mut ver_meta_buf = &meta[start..end];
|
|
|
|
ver_meta_buf.read_to_end(&mut ver.meta)?;
|
|
|
|
cur.set_position(end as u64);
|
|
|
|
self.versions.push(ver);
|
|
}
|
|
}
|
|
|
|
Ok(i)
|
|
}
|
|
|
|
// decode_xl_headers 解析 meta 头,返回 (versions 数量,xl_header_version, xl_meta_version, 已读数据长度)
|
|
#[tracing::instrument(level = "debug", skip_all)]
|
|
fn decode_xl_headers(buf: &[u8]) -> Result<(usize, u8, u8, &[u8])> {
|
|
let mut cur = Cursor::new(buf);
|
|
|
|
let header_ver: u8 = rmp::decode::read_int(&mut cur)?;
|
|
|
|
if header_ver > XL_HEADER_VERSION {
|
|
return Err(Error::msg("xl header version invalid"));
|
|
}
|
|
|
|
let meta_ver: u8 = rmp::decode::read_int(&mut cur)?;
|
|
if meta_ver > XL_META_VERSION {
|
|
return Err(Error::msg("xl meta version invalid"));
|
|
}
|
|
|
|
let versions_len: usize = rmp::decode::read_int(&mut cur)?;
|
|
|
|
Ok((versions_len, header_ver, meta_ver, &buf[cur.position() as usize..]))
|
|
}
|
|
|
|
fn decode_versions<F: FnMut(usize, &[u8], &[u8]) -> Result<()>>(buf: &[u8], versions: usize, mut fnc: F) -> Result<()> {
|
|
let mut cur: Cursor<&[u8]> = Cursor::new(buf);
|
|
|
|
for i in 0..versions {
|
|
let bin_len = rmp::decode::read_bin_len(&mut cur)? as usize;
|
|
let start = cur.position() as usize;
|
|
let end = start + bin_len;
|
|
let header_buf = &buf[start..end];
|
|
|
|
cur.set_position(end as u64);
|
|
|
|
let bin_len = rmp::decode::read_bin_len(&mut cur)? as usize;
|
|
let start = cur.position() as usize;
|
|
let end = start + bin_len;
|
|
let ver_meta_buf = &buf[start..end];
|
|
|
|
cur.set_position(end as u64);
|
|
|
|
if let Err(err) = fnc(i, header_buf, ver_meta_buf) {
|
|
if let Some(e) = err.downcast_ref::<StorageError>() {
|
|
if e == &StorageError::DoneForNow {
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
return Err(err);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn is_latest_delete_marker(buf: &[u8]) -> bool {
|
|
let header = Self::decode_xl_headers(buf).ok();
|
|
if let Some((versions, _hdr_v, _meta_v, meta)) = header {
|
|
if versions == 0 {
|
|
return false;
|
|
}
|
|
|
|
let mut is_delete_marker = false;
|
|
|
|
let _ = Self::decode_versions(meta, versions, |_: usize, hdr: &[u8], _: &[u8]| {
|
|
let mut header = FileMetaVersionHeader::default();
|
|
if header.unmarshal_msg(hdr).is_err() {
|
|
return Err(Error::new(StorageError::DoneForNow));
|
|
}
|
|
|
|
is_delete_marker = header.version_type == VersionType::Delete;
|
|
|
|
Err(Error::new(StorageError::DoneForNow))
|
|
});
|
|
|
|
is_delete_marker
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
#[tracing::instrument(level = "debug", skip_all)]
|
|
pub fn marshal_msg(&self) -> Result<Vec<u8>> {
|
|
let mut wr = Vec::new();
|
|
|
|
// header
|
|
wr.write_all(XL_FILE_HEADER.as_slice())?;
|
|
|
|
let mut major = [0u8; 2];
|
|
byteorder::LittleEndian::write_u16(&mut major, XL_FILE_VERSION_MAJOR);
|
|
wr.write_all(major.as_slice())?;
|
|
|
|
let mut minor = [0u8; 2];
|
|
byteorder::LittleEndian::write_u16(&mut minor, XL_FILE_VERSION_MINOR);
|
|
wr.write_all(minor.as_slice())?;
|
|
|
|
// size bin32 预留 write_bin_len
|
|
wr.write_all(&[0xc6, 0, 0, 0, 0])?;
|
|
|
|
let offset = wr.len();
|
|
|
|
rmp::encode::write_uint8(&mut wr, XL_HEADER_VERSION)?;
|
|
rmp::encode::write_uint8(&mut wr, XL_META_VERSION)?;
|
|
|
|
// versions
|
|
rmp::encode::write_sint(&mut wr, self.versions.len() as i64)?;
|
|
|
|
for ver in self.versions.iter() {
|
|
let hmsg = ver.header.marshal_msg()?;
|
|
rmp::encode::write_bin(&mut wr, &hmsg)?;
|
|
|
|
rmp::encode::write_bin(&mut wr, &ver.meta)?;
|
|
}
|
|
|
|
// 更新 bin 长度
|
|
let data_len = wr.len() - offset;
|
|
byteorder::BigEndian::write_u32(&mut wr[offset - 4..offset], data_len as u32);
|
|
|
|
let crc = xxh64::xxh64(&wr[offset..], XXHASH_SEED) as u32;
|
|
let mut crc_buf = [0u8; 5];
|
|
crc_buf[0] = 0xce; // u32
|
|
byteorder::BigEndian::write_u32(&mut crc_buf[1..], crc);
|
|
|
|
wr.write_all(&crc_buf)?;
|
|
|
|
wr.write_all(self.data.as_slice())?;
|
|
|
|
Ok(wr)
|
|
}
|
|
|
|
// pub fn unmarshal(buf: &[u8]) -> Result<Self> {
|
|
// let mut s = Self::default();
|
|
// s.unmarshal_msg(buf)?;
|
|
// Ok(s)
|
|
// // let t: FileMeta = rmp_serde::from_slice(buf)?;
|
|
// // Ok(t)
|
|
// }
|
|
|
|
// pub fn marshal_msg(&self) -> Result<Vec<u8>> {
|
|
// let mut buf = Vec::new();
|
|
|
|
// self.serialize(&mut Serializer::new(&mut buf))?;
|
|
|
|
// Ok(buf)
|
|
// }
|
|
|
|
fn get_idx(&self, idx: usize) -> Result<FileMetaVersion> {
|
|
if idx > self.versions.len() {
|
|
return Err(Error::new(DiskError::FileNotFound));
|
|
}
|
|
|
|
FileMetaVersion::try_from(self.versions[idx].meta.as_slice())
|
|
}
|
|
|
|
fn set_idx(&mut self, idx: usize, ver: FileMetaVersion) -> Result<()> {
|
|
if idx >= self.versions.len() {
|
|
return Err(Error::new(DiskError::FileNotFound));
|
|
}
|
|
|
|
// TODO: use old buf
|
|
let meta_buf = ver.marshal_msg()?;
|
|
|
|
let pre_mod_time = self.versions[idx].header.mod_time;
|
|
|
|
self.versions[idx].header = ver.header();
|
|
self.versions[idx].meta = meta_buf;
|
|
|
|
if pre_mod_time != self.versions[idx].header.mod_time {
|
|
self.sort_by_mod_time();
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn sort_by_mod_time(&mut self) {
|
|
if self.versions.len() <= 1 {
|
|
return;
|
|
}
|
|
|
|
// Sort by mod_time in descending order (latest first)
|
|
self.versions.sort_by(|a, b| {
|
|
match (a.header.mod_time, b.header.mod_time) {
|
|
(Some(a_time), Some(b_time)) => b_time.cmp(&a_time), // Descending order
|
|
(Some(_), None) => Ordering::Less,
|
|
(None, Some(_)) => Ordering::Greater,
|
|
(None, None) => Ordering::Equal,
|
|
}
|
|
});
|
|
}
|
|
|
|
// 查找版本
|
|
pub fn find_version(&self, vid: Option<Uuid>) -> Result<(usize, FileMetaVersion)> {
|
|
for (i, fver) in self.versions.iter().enumerate() {
|
|
if fver.header.version_id == vid {
|
|
let version = self.get_idx(i)?;
|
|
return Ok((i, version));
|
|
}
|
|
}
|
|
|
|
Err(Error::new(DiskError::FileVersionNotFound))
|
|
}
|
|
|
|
// shard_data_dir_count 查询 vid 下 data_dir 的数量
|
|
#[tracing::instrument(level = "debug", skip_all)]
|
|
pub fn shard_data_dir_count(&self, vid: &Option<Uuid>, data_dir: &Option<Uuid>) -> usize {
|
|
self.versions
|
|
.iter()
|
|
.filter(|v| v.header.version_type == VersionType::Object && v.header.version_id != *vid && v.header.user_data_dir())
|
|
.map(|v| FileMetaVersion::decode_data_dir_from_meta(&v.meta).unwrap_or_default())
|
|
.filter(|v| v == data_dir)
|
|
.count()
|
|
}
|
|
|
|
pub fn update_object_version(&mut self, fi: FileInfo) -> Result<()> {
|
|
for version in self.versions.iter_mut() {
|
|
match version.header.version_type {
|
|
VersionType::Invalid => (),
|
|
VersionType::Object => {
|
|
if version.header.version_id == fi.version_id {
|
|
let mut ver = FileMetaVersion::try_from(version.meta.as_slice())?;
|
|
|
|
if let Some(ref mut obj) = ver.object {
|
|
if let Some(ref mut meta_user) = obj.meta_user {
|
|
if let Some(meta) = &fi.metadata {
|
|
for (k, v) in meta {
|
|
meta_user.insert(k.clone(), v.clone());
|
|
}
|
|
}
|
|
obj.meta_user = Some(meta_user.clone());
|
|
} else {
|
|
let mut meta_user = HashMap::new();
|
|
if let Some(meta) = &fi.metadata {
|
|
for (k, v) in meta {
|
|
// TODO: MetaSys
|
|
meta_user.insert(k.clone(), v.clone());
|
|
}
|
|
}
|
|
obj.meta_user = Some(meta_user);
|
|
}
|
|
|
|
if let Some(mod_time) = fi.mod_time {
|
|
obj.mod_time = Some(mod_time);
|
|
}
|
|
}
|
|
|
|
// 更新
|
|
version.header = ver.header();
|
|
version.meta = ver.marshal_msg()?;
|
|
}
|
|
}
|
|
VersionType::Delete => {
|
|
if version.header.version_id == fi.version_id {
|
|
return Err(Error::msg("method not allowed"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.versions.sort_by(|a, b| {
|
|
if a.header.mod_time != b.header.mod_time {
|
|
a.header.mod_time.cmp(&b.header.mod_time)
|
|
} else if a.header.version_type != b.header.version_type {
|
|
a.header.version_type.cmp(&b.header.version_type)
|
|
} else if a.header.version_id != b.header.version_id {
|
|
a.header.version_id.cmp(&b.header.version_id)
|
|
} else if a.header.flags != b.header.flags {
|
|
a.header.flags.cmp(&b.header.flags)
|
|
} else {
|
|
a.cmp(b)
|
|
}
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
// 添加版本
|
|
#[tracing::instrument(level = "debug", skip_all)]
|
|
pub fn add_version(&mut self, fi: FileInfo) -> Result<()> {
|
|
let vid = fi.version_id;
|
|
|
|
if let Some(ref data) = fi.data {
|
|
let key = vid.unwrap_or_default().to_string();
|
|
self.data.replace(&key, data.clone())?;
|
|
}
|
|
|
|
let version = FileMetaVersion::from(fi);
|
|
|
|
if !version.valid() {
|
|
return Err(Error::msg("file meta version invalid"));
|
|
}
|
|
|
|
// should replace
|
|
for (idx, ver) in self.versions.iter().enumerate() {
|
|
if ver.header.version_id != vid {
|
|
continue;
|
|
}
|
|
|
|
return self.set_idx(idx, version);
|
|
}
|
|
|
|
// TODO: version count limit !
|
|
|
|
let mod_time = version.get_mod_time();
|
|
|
|
// puth a -1 mod time value , so we can relplace this
|
|
self.versions.push(FileMetaShallowVersion {
|
|
header: FileMetaVersionHeader {
|
|
mod_time: Some(OffsetDateTime::from_unix_timestamp(-1)?),
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
});
|
|
|
|
for (idx, exist) in self.versions.iter().enumerate() {
|
|
if let Some(ref ex_mt) = exist.header.mod_time {
|
|
if let Some(ref in_md) = mod_time {
|
|
if ex_mt <= in_md {
|
|
// insert
|
|
self.versions.insert(idx, FileMetaShallowVersion::try_from(version)?);
|
|
self.versions.pop();
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(Error::msg("add_version failed"))
|
|
}
|
|
|
|
// delete_version 删除版本,返回 data_dir
|
|
pub fn delete_version(&mut self, fi: &FileInfo) -> Result<Option<Uuid>> {
|
|
let mut ventry = FileMetaVersion::default();
|
|
if fi.deleted {
|
|
ventry.version_type = VersionType::Delete;
|
|
ventry.delete_marker = Some(MetaDeleteMarker {
|
|
version_id: fi.version_id,
|
|
mod_time: fi.mod_time,
|
|
..Default::default()
|
|
});
|
|
|
|
if !fi.is_valid() {
|
|
return Err(Error::msg("invalid file meta version"));
|
|
}
|
|
}
|
|
|
|
for (i, ver) in self.versions.iter().enumerate() {
|
|
if ver.header.version_id != fi.version_id {
|
|
continue;
|
|
}
|
|
|
|
return match ver.header.version_type {
|
|
VersionType::Invalid => Err(Error::msg("invalid file meta version")),
|
|
VersionType::Delete => Ok(None),
|
|
VersionType::Object => {
|
|
let v = self.get_idx(i)?;
|
|
|
|
self.versions.remove(i);
|
|
|
|
let a = v.object.map(|v| v.data_dir).unwrap_or_default();
|
|
Ok(a)
|
|
}
|
|
};
|
|
}
|
|
|
|
Err(Error::new(DiskError::FileVersionNotFound))
|
|
}
|
|
|
|
// read_data fill fi.dada
|
|
#[tracing::instrument(level = "debug", skip(self))]
|
|
pub fn into_fileinfo(
|
|
&self,
|
|
volume: &str,
|
|
path: &str,
|
|
version_id: &str,
|
|
read_data: bool,
|
|
all_parts: bool,
|
|
) -> Result<FileInfo> {
|
|
let has_vid = {
|
|
if !version_id.is_empty() {
|
|
let id = Uuid::parse_str(version_id)?;
|
|
if !id.is_nil() {
|
|
Some(id)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
|
|
let mut is_latest = true;
|
|
let mut succ_mod_time = None;
|
|
for ver in self.versions.iter() {
|
|
let header = &ver.header;
|
|
|
|
if let Some(vid) = has_vid {
|
|
if header.version_id != Some(vid) {
|
|
is_latest = false;
|
|
succ_mod_time = header.mod_time;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let mut fi = ver.to_fileinfo(volume, path, has_vid, all_parts)?;
|
|
fi.is_latest = is_latest;
|
|
if let Some(_d) = succ_mod_time {
|
|
fi.successor_mod_time = succ_mod_time;
|
|
}
|
|
if read_data {
|
|
fi.data = self.data.find(fi.version_id.unwrap_or_default().to_string().as_str())?;
|
|
}
|
|
|
|
fi.num_versions = self.versions.len();
|
|
|
|
return Ok(fi);
|
|
}
|
|
|
|
if has_vid.is_none() {
|
|
Err(Error::from(DiskError::FileNotFound))
|
|
} else {
|
|
Err(Error::from(DiskError::FileVersionNotFound))
|
|
}
|
|
}
|
|
|
|
#[tracing::instrument(level = "debug", skip(self))]
|
|
pub fn into_file_info_versions(&self, volume: &str, path: &str, all_parts: bool) -> Result<FileInfoVersions> {
|
|
let mut versions = Vec::new();
|
|
for version in self.versions.iter() {
|
|
let mut file_version = FileMetaVersion::default();
|
|
file_version.unmarshal_msg(&version.meta)?;
|
|
let fi = file_version.to_fileinfo(volume, path, None, all_parts);
|
|
versions.push(fi);
|
|
}
|
|
|
|
let mut prev_mod_time = None;
|
|
for (i, fi) in versions.iter_mut().enumerate() {
|
|
if i == 0 {
|
|
fi.is_latest = true;
|
|
} else {
|
|
fi.successor_mod_time = prev_mod_time;
|
|
}
|
|
prev_mod_time = fi.mod_time;
|
|
}
|
|
|
|
if versions.is_empty() {
|
|
versions.push(FileInfo {
|
|
name: path.to_string(),
|
|
volume: volume.to_string(),
|
|
deleted: true,
|
|
is_latest: true,
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
Ok(FileInfoVersions {
|
|
volume: volume.to_string(),
|
|
name: path.to_string(),
|
|
latest_mod_time: versions[0].mod_time,
|
|
versions,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
pub fn lastest_mod_time(&self) -> Option<OffsetDateTime> {
|
|
if self.versions.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
self.versions.first().unwrap().header.mod_time
|
|
}
|
|
}
|
|
|
|
// impl Display for FileMeta {
|
|
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
// f.write_str("FileMeta:")?;
|
|
// for (i, ver) in self.versions.iter().enumerate() {
|
|
// let mut meta = FileMetaVersion::default();
|
|
// meta.unmarshal_msg(&ver.meta).unwrap_or_default();
|
|
// f.write_fmt(format_args!("ver:{} header {:?}, meta {:?}", i, ver.header, meta))?;
|
|
// }
|
|
|
|
// f.write_str("\n")
|
|
// }
|
|
// }
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone, Eq, PartialOrd, Ord)]
|
|
pub struct FileMetaShallowVersion {
|
|
pub header: FileMetaVersionHeader,
|
|
pub meta: Vec<u8>, // FileMetaVersion.marshal_msg
|
|
}
|
|
|
|
impl FileMetaShallowVersion {
|
|
pub fn to_fileinfo(&self, volume: &str, path: &str, version_id: Option<Uuid>, all_parts: bool) -> Result<FileInfo> {
|
|
let file_version = FileMetaVersion::try_from(self.meta.as_slice())?;
|
|
|
|
Ok(file_version.to_fileinfo(volume, path, version_id, all_parts))
|
|
}
|
|
}
|
|
|
|
impl TryFrom<FileMetaVersion> for FileMetaShallowVersion {
|
|
type Error = Error;
|
|
|
|
fn try_from(value: FileMetaVersion) -> std::result::Result<Self, Self::Error> {
|
|
let header = value.header();
|
|
let meta = value.marshal_msg()?;
|
|
Ok(Self { meta, header })
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
|
pub struct FileMetaVersion {
|
|
pub version_type: VersionType,
|
|
pub object: Option<MetaObject>,
|
|
pub delete_marker: Option<MetaDeleteMarker>,
|
|
pub write_version: u64, // rustfs version
|
|
}
|
|
|
|
impl FileMetaVersion {
|
|
pub fn valid(&self) -> bool {
|
|
if !self.version_type.valid() {
|
|
return false;
|
|
}
|
|
|
|
match self.version_type {
|
|
VersionType::Object => self
|
|
.object
|
|
.as_ref()
|
|
.map(|v| v.erasure_algorithm.valid() && v.bitrot_checksum_algo.valid() && v.mod_time.is_some())
|
|
.unwrap_or_default(),
|
|
VersionType::Delete => self
|
|
.delete_marker
|
|
.as_ref()
|
|
.map(|v| v.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH) > OffsetDateTime::UNIX_EPOCH)
|
|
.unwrap_or_default(),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub fn get_data_dir(&self) -> Option<Uuid> {
|
|
self.valid()
|
|
.then(|| {
|
|
if self.version_type == VersionType::Object {
|
|
self.object.as_ref().map(|v| v.data_dir).unwrap_or_default()
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn get_version_id(&self) -> Option<Uuid> {
|
|
match self.version_type {
|
|
VersionType::Object | VersionType::Delete => self.object.as_ref().map(|v| v.version_id).unwrap_or_default(),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn get_mod_time(&self) -> Option<OffsetDateTime> {
|
|
match self.version_type {
|
|
VersionType::Object => self.object.as_ref().map(|v| v.mod_time).unwrap_or_default(),
|
|
VersionType::Delete => self.delete_marker.as_ref().map(|v| v.mod_time).unwrap_or_default(),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
// decode_data_dir_from_meta 从 meta 中读取 data_dir TODO: 直接从 meta buf 中只解析出 data_dir, msg.skip
|
|
pub fn decode_data_dir_from_meta(buf: &[u8]) -> Result<Option<Uuid>> {
|
|
let mut ver = Self::default();
|
|
ver.unmarshal_msg(buf)?;
|
|
|
|
let data_dir = ver.object.map(|v| v.data_dir).unwrap_or_default();
|
|
Ok(data_dir)
|
|
}
|
|
|
|
pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
|
|
let mut cur = Cursor::new(buf);
|
|
|
|
let mut fields_len = rmp::decode::read_map_len(&mut cur)?;
|
|
|
|
while fields_len > 0 {
|
|
fields_len -= 1;
|
|
|
|
// println!("unmarshal_msg fields idx {}", fields_len);
|
|
|
|
let str_len = rmp::decode::read_str_len(&mut cur)?;
|
|
|
|
// println!("unmarshal_msg fields name len() {}", &str_len);
|
|
|
|
// !!!Vec::with_capacity(str_len) 失败,vec! 正常
|
|
let mut field_buff = vec![0u8; str_len as usize];
|
|
|
|
cur.read_exact(&mut field_buff)?;
|
|
|
|
let field = String::from_utf8(field_buff)?;
|
|
|
|
// println!("unmarshal_msg fields name {}", &field);
|
|
|
|
match field.as_str() {
|
|
"Type" => {
|
|
let u: u8 = rmp::decode::read_int(&mut cur)?;
|
|
self.version_type = VersionType::from_u8(u);
|
|
}
|
|
|
|
"V2Obj" => {
|
|
// is_nil()
|
|
if buf[cur.position() as usize] == 0xc0 {
|
|
rmp::decode::read_nil(&mut cur)?;
|
|
} else {
|
|
// let buf = unsafe { cur.position() };
|
|
let mut obj = MetaObject::default();
|
|
// let start = cur.position();
|
|
|
|
let (_, remain) = buf.split_at(cur.position() as usize);
|
|
|
|
let read_len = obj.unmarshal_msg(remain)?;
|
|
cur.set_position(cur.position() + read_len);
|
|
|
|
self.object = Some(obj);
|
|
}
|
|
}
|
|
"DelObj" => {
|
|
if buf[cur.position() as usize] == 0xc0 {
|
|
rmp::decode::read_nil(&mut cur)?;
|
|
} else {
|
|
// let buf = unsafe { cur.position() };
|
|
let mut obj = MetaDeleteMarker::default();
|
|
// let start = cur.position();
|
|
|
|
let (_, remain) = buf.split_at(cur.position() as usize);
|
|
let read_len = obj.unmarshal_msg(remain)?;
|
|
cur.set_position(cur.position() + read_len);
|
|
|
|
self.delete_marker = Some(obj);
|
|
}
|
|
}
|
|
"v" => {
|
|
self.write_version = rmp::decode::read_int(&mut cur)?;
|
|
}
|
|
name => return Err(Error::msg(format!("not suport field name {}", name))),
|
|
}
|
|
}
|
|
|
|
Ok(cur.position())
|
|
}
|
|
|
|
pub fn marshal_msg(&self) -> Result<Vec<u8>> {
|
|
let mut len: u32 = 4;
|
|
let mut mask: u8 = 0;
|
|
|
|
if self.object.is_none() {
|
|
len -= 1;
|
|
mask |= 0x2;
|
|
}
|
|
if self.delete_marker.is_none() {
|
|
len -= 1;
|
|
mask |= 0x4;
|
|
}
|
|
|
|
let mut wr = Vec::new();
|
|
|
|
// 字段数量
|
|
rmp::encode::write_map_len(&mut wr, len)?;
|
|
|
|
// write "Type"
|
|
rmp::encode::write_str(&mut wr, "Type")?;
|
|
rmp::encode::write_uint(&mut wr, self.version_type.to_u8() as u64)?;
|
|
|
|
if (mask & 0x2) == 0 {
|
|
// write V2Obj
|
|
rmp::encode::write_str(&mut wr, "V2Obj")?;
|
|
if self.object.is_none() {
|
|
let _ = rmp::encode::write_nil(&mut wr);
|
|
} else {
|
|
let buf = self.object.as_ref().unwrap().marshal_msg()?;
|
|
wr.write_all(&buf)?;
|
|
}
|
|
}
|
|
|
|
if (mask & 0x4) == 0 {
|
|
// write "DelObj"
|
|
rmp::encode::write_str(&mut wr, "DelObj")?;
|
|
if self.delete_marker.is_none() {
|
|
let _ = rmp::encode::write_nil(&mut wr);
|
|
} else {
|
|
let buf = self.delete_marker.as_ref().unwrap().marshal_msg()?;
|
|
wr.write_all(&buf)?;
|
|
}
|
|
}
|
|
|
|
// write "v"
|
|
rmp::encode::write_str(&mut wr, "v")?;
|
|
rmp::encode::write_uint(&mut wr, self.write_version)?;
|
|
|
|
Ok(wr)
|
|
}
|
|
|
|
pub fn free_version(&self) -> bool {
|
|
self.version_type == VersionType::Delete && self.delete_marker.as_ref().map(|m| m.free_version()).unwrap_or_default()
|
|
}
|
|
|
|
pub fn header(&self) -> FileMetaVersionHeader {
|
|
FileMetaVersionHeader::from(self.clone())
|
|
}
|
|
|
|
pub fn to_fileinfo(&self, volume: &str, path: &str, version_id: Option<Uuid>, all_parts: bool) -> FileInfo {
|
|
match self.version_type {
|
|
VersionType::Invalid => FileInfo {
|
|
name: path.to_string(),
|
|
volume: volume.to_string(),
|
|
version_id,
|
|
..Default::default()
|
|
},
|
|
VersionType::Object => self
|
|
.object
|
|
.as_ref()
|
|
.unwrap()
|
|
.clone()
|
|
.into_fileinfo(volume, path, version_id, all_parts),
|
|
VersionType::Delete => self
|
|
.delete_marker
|
|
.as_ref()
|
|
.unwrap()
|
|
.clone()
|
|
.into_fileinfo(volume, path, version_id, all_parts),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&[u8]> for FileMetaVersion {
|
|
type Error = Error;
|
|
|
|
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
|
|
let mut ver = FileMetaVersion::default();
|
|
ver.unmarshal_msg(value)?;
|
|
Ok(ver)
|
|
}
|
|
}
|
|
|
|
impl From<FileInfo> for FileMetaVersion {
|
|
fn from(value: FileInfo) -> Self {
|
|
{
|
|
if value.deleted {
|
|
FileMetaVersion {
|
|
version_type: VersionType::Delete,
|
|
delete_marker: Some(MetaDeleteMarker::from(value)),
|
|
object: None,
|
|
write_version: 0,
|
|
}
|
|
} else {
|
|
FileMetaVersion {
|
|
version_type: VersionType::Object,
|
|
delete_marker: None,
|
|
object: Some(MetaObject::from(value)),
|
|
write_version: 0,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<FileMetaShallowVersion> for FileMetaVersion {
|
|
type Error = Error;
|
|
|
|
fn try_from(value: FileMetaShallowVersion) -> std::result::Result<Self, Self::Error> {
|
|
FileMetaVersion::try_from(value.meta.as_slice())
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone, Eq, Hash)]
|
|
pub struct FileMetaVersionHeader {
|
|
pub version_id: Option<Uuid>,
|
|
pub mod_time: Option<OffsetDateTime>,
|
|
pub signature: [u8; 4],
|
|
pub version_type: VersionType,
|
|
pub flags: u8,
|
|
pub ec_n: u8,
|
|
pub ec_m: u8,
|
|
}
|
|
|
|
impl FileMetaVersionHeader {
|
|
pub fn has_ec(&self) -> bool {
|
|
self.ec_m > 0 && self.ec_n > 0
|
|
}
|
|
|
|
pub fn matches_not_strict(&self, o: &FileMetaVersionHeader) -> bool {
|
|
let mut ok = self.version_id == o.version_id && self.version_type == o.version_type && self.matches_ec(o);
|
|
if self.version_id.is_none() {
|
|
ok = ok && self.mod_time == o.mod_time;
|
|
}
|
|
|
|
ok
|
|
}
|
|
|
|
pub fn matches_ec(&self, o: &FileMetaVersionHeader) -> bool {
|
|
if self.has_ec() && o.has_ec() {
|
|
return self.ec_n == o.ec_n && self.ec_m == o.ec_m;
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
pub fn free_version(&self) -> bool {
|
|
self.flags & XL_FLAG_FREE_VERSION != 0
|
|
}
|
|
|
|
pub fn sorts_before(&self, o: &FileMetaVersionHeader) -> bool {
|
|
if self == o {
|
|
return false;
|
|
}
|
|
|
|
// Prefer newest modtime.
|
|
if self.mod_time != o.mod_time {
|
|
return self.mod_time > o.mod_time;
|
|
}
|
|
|
|
match self.mod_time.cmp(&o.mod_time) {
|
|
Ordering::Greater => {
|
|
return true;
|
|
}
|
|
Ordering::Less => {
|
|
return false;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
// The following doesn't make too much sense, but we want sort to be consistent nonetheless.
|
|
// Prefer lower types
|
|
if self.version_type != o.version_type {
|
|
return self.version_type < o.version_type;
|
|
}
|
|
// Consistent sort on signature
|
|
match self.version_id.cmp(&o.version_id) {
|
|
Ordering::Greater => {
|
|
return true;
|
|
}
|
|
Ordering::Less => {
|
|
return false;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
if self.flags != o.flags {
|
|
return self.flags > o.flags;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
pub fn user_data_dir(&self) -> bool {
|
|
self.flags & Flags::UsesDataDir as u8 != 0
|
|
}
|
|
#[tracing::instrument]
|
|
pub fn marshal_msg(&self) -> Result<Vec<u8>> {
|
|
let mut wr = Vec::new();
|
|
|
|
// array len 7
|
|
rmp::encode::write_array_len(&mut wr, 7)?;
|
|
|
|
// version_id
|
|
rmp::encode::write_bin(&mut wr, self.version_id.unwrap_or_default().as_bytes())?;
|
|
// mod_time
|
|
rmp::encode::write_i64(&mut wr, self.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH).unix_timestamp_nanos() as i64)?;
|
|
// signature
|
|
rmp::encode::write_bin(&mut wr, self.signature.as_slice())?;
|
|
// version_type
|
|
rmp::encode::write_uint8(&mut wr, self.version_type.to_u8())?;
|
|
// flags
|
|
rmp::encode::write_uint8(&mut wr, self.flags)?;
|
|
// ec_n
|
|
rmp::encode::write_uint8(&mut wr, self.ec_n)?;
|
|
// ec_m
|
|
rmp::encode::write_uint8(&mut wr, self.ec_m)?;
|
|
|
|
Ok(wr)
|
|
}
|
|
|
|
pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
|
|
let mut cur = Cursor::new(buf);
|
|
let alen = rmp::decode::read_array_len(&mut cur)?;
|
|
if alen != 7 {
|
|
return Err(Error::msg(format!("version header array len err need 7 got {}", alen)));
|
|
}
|
|
|
|
// version_id
|
|
rmp::decode::read_bin_len(&mut cur)?;
|
|
let mut buf = [0u8; 16];
|
|
cur.read_exact(&mut buf)?;
|
|
self.version_id = {
|
|
let id = Uuid::from_bytes(buf);
|
|
if id.is_nil() {
|
|
None
|
|
} else {
|
|
Some(id)
|
|
}
|
|
};
|
|
|
|
// mod_time
|
|
let unix: i128 = rmp::decode::read_int(&mut cur)?;
|
|
|
|
let time = OffsetDateTime::from_unix_timestamp_nanos(unix)?;
|
|
if time == OffsetDateTime::UNIX_EPOCH {
|
|
self.mod_time = None;
|
|
} else {
|
|
self.mod_time = Some(time);
|
|
}
|
|
|
|
// signature
|
|
rmp::decode::read_bin_len(&mut cur)?;
|
|
cur.read_exact(&mut self.signature)?;
|
|
|
|
// version_type
|
|
let typ: u8 = rmp::decode::read_int(&mut cur)?;
|
|
self.version_type = VersionType::from_u8(typ);
|
|
|
|
// flags
|
|
self.flags = rmp::decode::read_int(&mut cur)?;
|
|
// ec_n
|
|
self.ec_n = rmp::decode::read_int(&mut cur)?;
|
|
// ec_m
|
|
self.ec_m = rmp::decode::read_int(&mut cur)?;
|
|
|
|
Ok(cur.position())
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for FileMetaVersionHeader {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for FileMetaVersionHeader {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
match self.mod_time.cmp(&other.mod_time) {
|
|
Ordering::Equal => {}
|
|
ord => return ord,
|
|
}
|
|
|
|
match self.version_type.cmp(&other.version_type) {
|
|
Ordering::Equal => {}
|
|
ord => return ord,
|
|
}
|
|
match self.signature.cmp(&other.signature) {
|
|
Ordering::Equal => {}
|
|
ord => return ord,
|
|
}
|
|
match self.version_id.cmp(&other.version_id) {
|
|
Ordering::Equal => {}
|
|
ord => return ord,
|
|
}
|
|
self.flags.cmp(&other.flags)
|
|
}
|
|
}
|
|
|
|
impl From<FileMetaVersion> for FileMetaVersionHeader {
|
|
fn from(value: FileMetaVersion) -> Self {
|
|
let flags = {
|
|
let mut f: u8 = 0;
|
|
if value.free_version() {
|
|
f |= Flags::FreeVersion as u8;
|
|
}
|
|
|
|
if value.version_type == VersionType::Object && value.object.as_ref().map(|v| v.use_data_dir()).unwrap_or_default() {
|
|
f |= Flags::UsesDataDir as u8;
|
|
}
|
|
|
|
if value.version_type == VersionType::Object && value.object.as_ref().map(|v| v.use_inlinedata()).unwrap_or_default()
|
|
{
|
|
f |= Flags::InlineData as u8;
|
|
}
|
|
|
|
f
|
|
};
|
|
|
|
let (ec_n, ec_m) = {
|
|
if value.version_type == VersionType::Object && value.object.is_some() {
|
|
(
|
|
value.object.as_ref().unwrap().erasure_n as u8,
|
|
value.object.as_ref().unwrap().erasure_m as u8,
|
|
)
|
|
} else {
|
|
(0, 0)
|
|
}
|
|
};
|
|
|
|
Self {
|
|
version_id: value.get_version_id(),
|
|
mod_time: value.get_mod_time(),
|
|
signature: [0, 0, 0, 0],
|
|
version_type: value.version_type,
|
|
flags,
|
|
ec_n,
|
|
ec_m,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
|
|
// 因为自定义 message_pack,所以一定要保证字段顺序
|
|
pub struct MetaObject {
|
|
pub version_id: Option<Uuid>, // Version ID
|
|
pub data_dir: Option<Uuid>, // Data dir ID
|
|
pub erasure_algorithm: ErasureAlgo, // Erasure coding algorithm
|
|
pub erasure_m: usize, // Erasure data blocks
|
|
pub erasure_n: usize, // Erasure parity blocks
|
|
pub erasure_block_size: usize, // Erasure block size
|
|
pub erasure_index: usize, // Erasure disk index
|
|
pub erasure_dist: Vec<u8>, // Erasure distribution
|
|
pub bitrot_checksum_algo: ChecksumAlgo, // Bitrot checksum algo
|
|
pub part_numbers: Vec<usize>, // Part Numbers
|
|
pub part_etags: Option<Vec<String>>, // Part ETags
|
|
pub part_sizes: Vec<usize>, // Part Sizes
|
|
pub part_actual_sizes: Option<Vec<usize>>, // Part ActualSizes (compression)
|
|
pub part_indices: Option<Vec<Vec<u8>>>, // Part Indexes (compression)
|
|
pub size: usize, // Object version size
|
|
pub mod_time: Option<OffsetDateTime>, // Object version modified time
|
|
pub meta_sys: Option<HashMap<String, Vec<u8>>>, // Object version internal metadata
|
|
pub meta_user: Option<HashMap<String, String>>, // Object version metadata set by user
|
|
}
|
|
|
|
impl MetaObject {
|
|
pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
|
|
let mut cur = Cursor::new(buf);
|
|
|
|
let mut fields_len = rmp::decode::read_map_len(&mut cur)?;
|
|
|
|
// let mut ret = Self::default();
|
|
|
|
while fields_len > 0 {
|
|
fields_len -= 1;
|
|
|
|
// println!("unmarshal_msg fields idx {}", fields_len);
|
|
|
|
let str_len = rmp::decode::read_str_len(&mut cur)?;
|
|
|
|
// println!("unmarshal_msg fields name len() {}", &str_len);
|
|
|
|
// !!!Vec::with_capacity(str_len) 失败,vec! 正常
|
|
let mut field_buff = vec![0u8; str_len as usize];
|
|
|
|
cur.read_exact(&mut field_buff)?;
|
|
|
|
let field = String::from_utf8(field_buff)?;
|
|
|
|
// println!("unmarshal_msg fields name {}", &field);
|
|
|
|
match field.as_str() {
|
|
"ID" => {
|
|
rmp::decode::read_bin_len(&mut cur)?;
|
|
let mut buf = [0u8; 16];
|
|
cur.read_exact(&mut buf)?;
|
|
self.version_id = {
|
|
let id = Uuid::from_bytes(buf);
|
|
if id.is_nil() {
|
|
None
|
|
} else {
|
|
Some(id)
|
|
}
|
|
};
|
|
}
|
|
"DDir" => {
|
|
rmp::decode::read_bin_len(&mut cur)?;
|
|
let mut buf = [0u8; 16];
|
|
cur.read_exact(&mut buf)?;
|
|
self.data_dir = {
|
|
let id = Uuid::from_bytes(buf);
|
|
if id.is_nil() {
|
|
None
|
|
} else {
|
|
Some(id)
|
|
}
|
|
};
|
|
}
|
|
"EcAlgo" => {
|
|
let u: u8 = rmp::decode::read_int(&mut cur)?;
|
|
self.erasure_algorithm = ErasureAlgo::from_u8(u)
|
|
}
|
|
"EcM" => {
|
|
self.erasure_m = rmp::decode::read_int(&mut cur)?;
|
|
}
|
|
"EcN" => {
|
|
self.erasure_n = rmp::decode::read_int(&mut cur)?;
|
|
}
|
|
"EcBSize" => {
|
|
self.erasure_block_size = rmp::decode::read_int(&mut cur)?;
|
|
}
|
|
"EcIndex" => {
|
|
self.erasure_index = rmp::decode::read_int(&mut cur)?;
|
|
}
|
|
"EcDist" => {
|
|
let alen = rmp::decode::read_array_len(&mut cur)? as usize;
|
|
self.erasure_dist = vec![0u8; alen];
|
|
for i in 0..alen {
|
|
self.erasure_dist[i] = rmp::decode::read_int(&mut cur)?;
|
|
}
|
|
}
|
|
"CSumAlgo" => {
|
|
let u: u8 = rmp::decode::read_int(&mut cur)?;
|
|
self.bitrot_checksum_algo = ChecksumAlgo::from_u8(u)
|
|
}
|
|
"PartNums" => {
|
|
let alen = rmp::decode::read_array_len(&mut cur)? as usize;
|
|
self.part_numbers = vec![0; alen];
|
|
for i in 0..alen {
|
|
self.part_numbers[i] = rmp::decode::read_int(&mut cur)?;
|
|
}
|
|
}
|
|
"PartETags" => {
|
|
let array_len = match rmp::decode::read_nil(&mut cur) {
|
|
Ok(_) => None,
|
|
Err(e) => match e {
|
|
rmp::decode::ValueReadError::TypeMismatch(marker) => match marker {
|
|
Marker::FixArray(l) => Some(l as usize),
|
|
Marker::Array16 => Some(rmp::decode::read_u16(&mut cur)? as usize),
|
|
Marker::Array32 => Some(rmp::decode::read_u16(&mut cur)? as usize),
|
|
_ => return Err(Error::msg("PartETags parse failed")),
|
|
},
|
|
_ => return Err(Error::msg("PartETags parse failed.")),
|
|
},
|
|
};
|
|
|
|
if array_len.is_some() {
|
|
let l = array_len.unwrap();
|
|
let mut etags = Vec::with_capacity(l);
|
|
for _ in 0..l {
|
|
let str_len = rmp::decode::read_str_len(&mut cur)?;
|
|
let mut field_buff = vec![0u8; str_len as usize];
|
|
cur.read_exact(&mut field_buff)?;
|
|
etags.push(String::from_utf8(field_buff)?);
|
|
}
|
|
self.part_etags = Some(etags);
|
|
}
|
|
}
|
|
"PartSizes" => {
|
|
let alen = rmp::decode::read_array_len(&mut cur)? as usize;
|
|
self.part_sizes = vec![0; alen];
|
|
for i in 0..alen {
|
|
self.part_sizes[i] = rmp::decode::read_int(&mut cur)?;
|
|
}
|
|
}
|
|
"PartASizes" => {
|
|
let array_len = match rmp::decode::read_nil(&mut cur) {
|
|
Ok(_) => None,
|
|
Err(e) => match e {
|
|
rmp::decode::ValueReadError::TypeMismatch(marker) => match marker {
|
|
Marker::FixArray(l) => Some(l as usize),
|
|
Marker::Array16 => Some(rmp::decode::read_u16(&mut cur)? as usize),
|
|
Marker::Array32 => Some(rmp::decode::read_u16(&mut cur)? as usize),
|
|
_ => return Err(Error::msg("PartETags parse failed")),
|
|
},
|
|
_ => return Err(Error::msg("PartETags parse failed.")),
|
|
},
|
|
};
|
|
if let Some(l) = array_len {
|
|
let mut sizes = vec![0; l];
|
|
for size in sizes.iter_mut().take(l) {
|
|
*size = rmp::decode::read_int(&mut cur)?;
|
|
}
|
|
// for size in sizes.iter_mut().take(l) {
|
|
// let tmp = rmp::decode::read_int(&mut cur)?;
|
|
// size = tmp;
|
|
// }
|
|
self.part_actual_sizes = Some(sizes);
|
|
}
|
|
}
|
|
"PartIdx" => {
|
|
let alen = rmp::decode::read_array_len(&mut cur)? as usize;
|
|
|
|
if alen == 0 {
|
|
self.part_indices = None;
|
|
continue;
|
|
}
|
|
|
|
let mut indices = Vec::with_capacity(alen);
|
|
for _ in 0..alen {
|
|
let blen = rmp::decode::read_bin_len(&mut cur)?;
|
|
let mut buf = vec![0u8; blen as usize];
|
|
cur.read_exact(&mut buf)?;
|
|
|
|
indices.push(buf);
|
|
}
|
|
|
|
self.part_indices = Some(indices);
|
|
}
|
|
"Size" => {
|
|
self.size = rmp::decode::read_int(&mut cur)?;
|
|
}
|
|
"MTime" => {
|
|
let unix: i128 = rmp::decode::read_int(&mut cur)?;
|
|
let time = OffsetDateTime::from_unix_timestamp_nanos(unix)?;
|
|
if time == OffsetDateTime::UNIX_EPOCH {
|
|
self.mod_time = None;
|
|
} else {
|
|
self.mod_time = Some(time);
|
|
}
|
|
}
|
|
"MetaSys" => {
|
|
let len = match rmp::decode::read_nil(&mut cur) {
|
|
Ok(_) => None,
|
|
Err(e) => match e {
|
|
rmp::decode::ValueReadError::TypeMismatch(marker) => match marker {
|
|
Marker::FixMap(l) => Some(l as usize),
|
|
Marker::Map16 => Some(rmp::decode::read_u16(&mut cur)? as usize),
|
|
Marker::Map32 => Some(rmp::decode::read_u16(&mut cur)? as usize),
|
|
_ => return Err(Error::msg("MetaSys parse failed")),
|
|
},
|
|
_ => return Err(Error::msg("MetaSys parse failed.")),
|
|
},
|
|
};
|
|
if len.is_some() {
|
|
let l = len.unwrap();
|
|
let mut map = HashMap::new();
|
|
for _ in 0..l {
|
|
let str_len = rmp::decode::read_str_len(&mut cur)?;
|
|
let mut field_buff = vec![0u8; str_len as usize];
|
|
cur.read_exact(&mut field_buff)?;
|
|
let key = String::from_utf8(field_buff)?;
|
|
|
|
let blen = rmp::decode::read_bin_len(&mut cur)?;
|
|
let mut val = vec![0u8; blen as usize];
|
|
cur.read_exact(&mut val)?;
|
|
|
|
map.insert(key, val);
|
|
}
|
|
|
|
self.meta_sys = Some(map);
|
|
}
|
|
}
|
|
"MetaUsr" => {
|
|
let len = match rmp::decode::read_nil(&mut cur) {
|
|
Ok(_) => None,
|
|
Err(e) => match e {
|
|
rmp::decode::ValueReadError::TypeMismatch(marker) => match marker {
|
|
Marker::FixMap(l) => Some(l as usize),
|
|
Marker::Map16 => Some(rmp::decode::read_u16(&mut cur)? as usize),
|
|
Marker::Map32 => Some(rmp::decode::read_u16(&mut cur)? as usize),
|
|
_ => return Err(Error::msg("MetaUsr parse failed")),
|
|
},
|
|
_ => return Err(Error::msg("MetaUsr parse failed.")),
|
|
},
|
|
};
|
|
if len.is_some() {
|
|
let l = len.unwrap();
|
|
let mut map = HashMap::new();
|
|
for _ in 0..l {
|
|
let str_len = rmp::decode::read_str_len(&mut cur)?;
|
|
let mut field_buff = vec![0u8; str_len as usize];
|
|
cur.read_exact(&mut field_buff)?;
|
|
let key = String::from_utf8(field_buff)?;
|
|
|
|
let blen = rmp::decode::read_str_len(&mut cur)?;
|
|
let mut val_buf = vec![0u8; blen as usize];
|
|
cur.read_exact(&mut val_buf)?;
|
|
let val = String::from_utf8(val_buf)?;
|
|
|
|
map.insert(key, val);
|
|
}
|
|
|
|
self.meta_user = Some(map);
|
|
}
|
|
}
|
|
|
|
name => return Err(Error::msg(format!("not suport field name {}", name))),
|
|
}
|
|
}
|
|
|
|
Ok(cur.position())
|
|
}
|
|
// marshal_msg 自定义 messagepack 命名与 go 一致
|
|
pub fn marshal_msg(&self) -> Result<Vec<u8>> {
|
|
let mut len: u32 = 18;
|
|
let mut mask: u32 = 0;
|
|
|
|
if self.part_indices.is_none() {
|
|
len -= 1;
|
|
mask |= 0x2000;
|
|
}
|
|
|
|
let mut wr = Vec::new();
|
|
|
|
// 字段数量
|
|
rmp::encode::write_map_len(&mut wr, len)?;
|
|
|
|
// string "ID"
|
|
rmp::encode::write_str(&mut wr, "ID")?;
|
|
rmp::encode::write_bin(&mut wr, self.version_id.unwrap_or_default().as_bytes())?;
|
|
|
|
// string "DDir"
|
|
rmp::encode::write_str(&mut wr, "DDir")?;
|
|
rmp::encode::write_bin(&mut wr, self.data_dir.unwrap_or_default().as_bytes())?;
|
|
|
|
// string "EcAlgo"
|
|
rmp::encode::write_str(&mut wr, "EcAlgo")?;
|
|
rmp::encode::write_uint(&mut wr, self.erasure_algorithm.to_u8() as u64)?;
|
|
|
|
// string "EcM"
|
|
rmp::encode::write_str(&mut wr, "EcM")?;
|
|
rmp::encode::write_uint(&mut wr, self.erasure_m.try_into().unwrap())?;
|
|
|
|
// string "EcN"
|
|
rmp::encode::write_str(&mut wr, "EcN")?;
|
|
rmp::encode::write_uint(&mut wr, self.erasure_n.try_into().unwrap())?;
|
|
|
|
// string "EcBSize"
|
|
rmp::encode::write_str(&mut wr, "EcBSize")?;
|
|
rmp::encode::write_uint(&mut wr, self.erasure_block_size.try_into().unwrap())?;
|
|
|
|
// string "EcIndex"
|
|
rmp::encode::write_str(&mut wr, "EcIndex")?;
|
|
rmp::encode::write_uint(&mut wr, self.erasure_index.try_into().unwrap())?;
|
|
|
|
// string "EcDist"
|
|
rmp::encode::write_str(&mut wr, "EcDist")?;
|
|
rmp::encode::write_array_len(&mut wr, self.erasure_dist.len() as u32)?;
|
|
for v in self.erasure_dist.iter() {
|
|
rmp::encode::write_uint(&mut wr, *v as _)?;
|
|
}
|
|
|
|
// string "CSumAlgo"
|
|
rmp::encode::write_str(&mut wr, "CSumAlgo")?;
|
|
rmp::encode::write_uint(&mut wr, self.bitrot_checksum_algo.to_u8() as u64)?;
|
|
|
|
// string "PartNums"
|
|
rmp::encode::write_str(&mut wr, "PartNums")?;
|
|
rmp::encode::write_array_len(&mut wr, self.part_numbers.len() as u32)?;
|
|
for v in self.part_numbers.iter() {
|
|
rmp::encode::write_uint(&mut wr, *v as _)?;
|
|
}
|
|
|
|
// string "PartETags"
|
|
rmp::encode::write_str(&mut wr, "PartETags")?;
|
|
if self.part_etags.is_none() {
|
|
rmp::encode::write_nil(&mut wr)?;
|
|
} else {
|
|
let etags = self.part_etags.as_ref().unwrap();
|
|
rmp::encode::write_array_len(&mut wr, etags.len() as u32)?;
|
|
for v in etags.iter() {
|
|
rmp::encode::write_str(&mut wr, v.as_str())?;
|
|
}
|
|
}
|
|
|
|
// string "PartSizes"
|
|
rmp::encode::write_str(&mut wr, "PartSizes")?;
|
|
rmp::encode::write_array_len(&mut wr, self.part_sizes.len() as u32)?;
|
|
for v in self.part_sizes.iter() {
|
|
rmp::encode::write_uint(&mut wr, *v as _)?;
|
|
}
|
|
|
|
// string "PartASizes"
|
|
rmp::encode::write_str(&mut wr, "PartASizes")?;
|
|
if self.part_actual_sizes.is_none() {
|
|
rmp::encode::write_nil(&mut wr)?;
|
|
} else {
|
|
let asizes = self.part_actual_sizes.as_ref().unwrap();
|
|
rmp::encode::write_array_len(&mut wr, asizes.len() as u32)?;
|
|
for v in asizes.iter() {
|
|
rmp::encode::write_uint(&mut wr, *v as _)?;
|
|
}
|
|
}
|
|
|
|
if (mask & 0x2000) == 0 {
|
|
// string "PartIdx"
|
|
rmp::encode::write_str(&mut wr, "PartIdx")?;
|
|
let indices = self.part_indices.as_ref().unwrap();
|
|
rmp::encode::write_array_len(&mut wr, indices.len() as u32)?;
|
|
for v in indices.iter() {
|
|
rmp::encode::write_bin(&mut wr, v)?;
|
|
}
|
|
}
|
|
|
|
// string "Size"
|
|
rmp::encode::write_str(&mut wr, "Size")?;
|
|
rmp::encode::write_uint(&mut wr, self.size.try_into().unwrap())?;
|
|
|
|
// string "MTime"
|
|
rmp::encode::write_str(&mut wr, "MTime")?;
|
|
rmp::encode::write_uint(
|
|
&mut wr,
|
|
self.mod_time
|
|
.unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
|
.unix_timestamp_nanos()
|
|
.try_into()
|
|
.unwrap(),
|
|
)?;
|
|
|
|
// string "MetaSys"
|
|
rmp::encode::write_str(&mut wr, "MetaSys")?;
|
|
if self.meta_sys.is_none() {
|
|
rmp::encode::write_nil(&mut wr)?;
|
|
} else {
|
|
let metas = self.meta_sys.as_ref().unwrap();
|
|
rmp::encode::write_map_len(&mut wr, metas.len() as u32)?;
|
|
for (k, v) in metas {
|
|
rmp::encode::write_str(&mut wr, k.as_str())?;
|
|
rmp::encode::write_bin(&mut wr, v)?;
|
|
}
|
|
}
|
|
|
|
// string "MetaUsr"
|
|
rmp::encode::write_str(&mut wr, "MetaUsr")?;
|
|
if self.meta_user.is_none() {
|
|
rmp::encode::write_nil(&mut wr)?;
|
|
} else {
|
|
let metas = self.meta_user.as_ref().unwrap();
|
|
rmp::encode::write_map_len(&mut wr, metas.len() as u32)?;
|
|
for (k, v) in metas {
|
|
rmp::encode::write_str(&mut wr, k.as_str())?;
|
|
rmp::encode::write_str(&mut wr, v.as_str())?;
|
|
}
|
|
}
|
|
|
|
Ok(wr)
|
|
}
|
|
pub fn use_data_dir(&self) -> bool {
|
|
// TODO: when use inlinedata
|
|
true
|
|
}
|
|
|
|
pub fn use_inlinedata(&self) -> bool {
|
|
// TODO: when use inlinedata
|
|
false
|
|
}
|
|
|
|
pub fn into_fileinfo(self, volume: &str, path: &str, _version_id: Option<Uuid>, _all_parts: bool) -> FileInfo {
|
|
let version_id = self.version_id;
|
|
|
|
let erasure = ErasureInfo {
|
|
algorithm: self.erasure_algorithm.to_string(),
|
|
data_blocks: self.erasure_m,
|
|
parity_blocks: self.erasure_n,
|
|
block_size: self.erasure_block_size,
|
|
index: self.erasure_index,
|
|
distribution: self.erasure_dist.iter().map(|&v| v as usize).collect(),
|
|
..Default::default()
|
|
};
|
|
|
|
let mut parts = Vec::new();
|
|
for (i, _) in self.part_numbers.iter().enumerate() {
|
|
parts.push(ObjectPartInfo {
|
|
number: self.part_numbers[i],
|
|
size: self.part_sizes[i],
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
let metadata = {
|
|
if let Some(metauser) = self.meta_user.as_ref() {
|
|
let mut m = HashMap::new();
|
|
for (k, v) in metauser {
|
|
// TODO: skip xhttp x-amz-storage-class
|
|
m.insert(k.to_owned(), v.to_owned());
|
|
}
|
|
Some(m)
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
|
|
FileInfo {
|
|
version_id,
|
|
erasure,
|
|
data_dir: self.data_dir,
|
|
mod_time: self.mod_time,
|
|
size: self.size,
|
|
name: path.to_string(),
|
|
volume: volume.to_string(),
|
|
parts,
|
|
metadata,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<FileInfo> for MetaObject {
|
|
fn from(value: FileInfo) -> Self {
|
|
let part_numbers: Vec<usize> = value.parts.iter().map(|v| v.number).collect();
|
|
let part_sizes: Vec<usize> = value.parts.iter().map(|v| v.size).collect();
|
|
|
|
Self {
|
|
version_id: value.version_id,
|
|
size: value.size,
|
|
mod_time: value.mod_time,
|
|
data_dir: value.data_dir,
|
|
erasure_algorithm: ErasureAlgo::ReedSolomon,
|
|
erasure_m: value.erasure.data_blocks,
|
|
erasure_n: value.erasure.parity_blocks,
|
|
erasure_block_size: value.erasure.block_size,
|
|
erasure_index: value.erasure.index,
|
|
erasure_dist: value.erasure.distribution.iter().map(|x| *x as u8).collect(),
|
|
bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
|
|
part_numbers,
|
|
part_etags: None, // TODO: add part_etags
|
|
part_sizes,
|
|
part_actual_sizes: None, // TODO: add part_etags
|
|
part_indices: None,
|
|
meta_sys: None,
|
|
meta_user: value.metadata.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
|
|
pub struct MetaDeleteMarker {
|
|
pub version_id: Option<Uuid>, // Version ID for delete marker
|
|
pub mod_time: Option<OffsetDateTime>, // Object delete marker modified time
|
|
pub meta_sys: Option<HashMap<String, Vec<u8>>>, // Delete marker internal metadata
|
|
}
|
|
|
|
impl MetaDeleteMarker {
|
|
pub fn free_version(&self) -> bool {
|
|
self.meta_sys
|
|
.as_ref()
|
|
.map(|v| v.get(FREE_VERSION_META_HEADER).is_some())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn into_fileinfo(self, volume: &str, path: &str, version_id: Option<Uuid>, _all_parts: bool) -> FileInfo {
|
|
FileInfo {
|
|
name: path.to_string(),
|
|
volume: volume.to_string(),
|
|
version_id,
|
|
deleted: true,
|
|
mod_time: self.mod_time,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub fn unmarshal_msg(&mut self, buf: &[u8]) -> Result<u64> {
|
|
let mut cur = Cursor::new(buf);
|
|
|
|
let mut fields_len = rmp::decode::read_map_len(&mut cur)?;
|
|
|
|
while fields_len > 0 {
|
|
fields_len -= 1;
|
|
|
|
let str_len = rmp::decode::read_str_len(&mut cur)?;
|
|
|
|
// !!!Vec::with_capacity(str_len) 失败,vec! 正常
|
|
let mut field_buff = vec![0u8; str_len as usize];
|
|
|
|
cur.read_exact(&mut field_buff)?;
|
|
|
|
let field = String::from_utf8(field_buff)?;
|
|
|
|
match field.as_str() {
|
|
"ID" => {
|
|
rmp::decode::read_bin_len(&mut cur)?;
|
|
let mut buf = [0u8; 16];
|
|
cur.read_exact(&mut buf)?;
|
|
self.version_id = {
|
|
let id = Uuid::from_bytes(buf);
|
|
if id.is_nil() {
|
|
None
|
|
} else {
|
|
Some(id)
|
|
}
|
|
};
|
|
}
|
|
|
|
"MTime" => {
|
|
let unix: i64 = rmp::decode::read_int(&mut cur)?;
|
|
let time = OffsetDateTime::from_unix_timestamp(unix)?;
|
|
if time == OffsetDateTime::UNIX_EPOCH {
|
|
self.mod_time = None;
|
|
} else {
|
|
self.mod_time = Some(time);
|
|
}
|
|
}
|
|
"MetaSys" => {
|
|
let l = rmp::decode::read_map_len(&mut cur)?;
|
|
let mut map = HashMap::new();
|
|
for _ in 0..l {
|
|
let str_len = rmp::decode::read_str_len(&mut cur)?;
|
|
let mut field_buff = vec![0u8; str_len as usize];
|
|
cur.read_exact(&mut field_buff)?;
|
|
let key = String::from_utf8(field_buff)?;
|
|
|
|
let blen = rmp::decode::read_bin_len(&mut cur)?;
|
|
let mut val = vec![0u8; blen as usize];
|
|
cur.read_exact(&mut val)?;
|
|
|
|
map.insert(key, val);
|
|
}
|
|
|
|
self.meta_sys = Some(map);
|
|
}
|
|
name => return Err(Error::msg(format!("not suport field name {}", name))),
|
|
}
|
|
}
|
|
|
|
Ok(cur.position())
|
|
}
|
|
|
|
pub fn marshal_msg(&self) -> Result<Vec<u8>> {
|
|
let mut len: u32 = 3;
|
|
let mut mask: u8 = 0;
|
|
|
|
if self.meta_sys.is_none() {
|
|
len -= 1;
|
|
mask |= 0x4;
|
|
}
|
|
|
|
let mut wr = Vec::new();
|
|
|
|
// 字段数量
|
|
rmp::encode::write_map_len(&mut wr, len)?;
|
|
|
|
// string "ID"
|
|
rmp::encode::write_str(&mut wr, "ID")?;
|
|
rmp::encode::write_bin(&mut wr, self.version_id.unwrap_or_default().as_bytes())?;
|
|
|
|
// string "MTime"
|
|
rmp::encode::write_str(&mut wr, "MTime")?;
|
|
rmp::encode::write_uint(
|
|
&mut wr,
|
|
self.mod_time
|
|
.unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
|
.unix_timestamp()
|
|
.try_into()
|
|
.unwrap(),
|
|
)?;
|
|
|
|
if (mask & 0x4) == 0 {
|
|
let metas = self.meta_sys.as_ref().unwrap();
|
|
rmp::encode::write_map_len(&mut wr, metas.len() as u32)?;
|
|
for (k, v) in metas {
|
|
rmp::encode::write_str(&mut wr, k.as_str())?;
|
|
rmp::encode::write_bin(&mut wr, v)?;
|
|
}
|
|
}
|
|
|
|
Ok(wr)
|
|
}
|
|
}
|
|
|
|
impl From<FileInfo> for MetaDeleteMarker {
|
|
fn from(value: FileInfo) -> Self {
|
|
Self {
|
|
version_id: value.version_id,
|
|
mod_time: value.mod_time,
|
|
meta_sys: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default, Clone, PartialOrd, Ord, Hash)]
|
|
pub enum VersionType {
|
|
#[default]
|
|
Invalid = 0,
|
|
Object = 1,
|
|
Delete = 2,
|
|
// Legacy = 3,
|
|
}
|
|
|
|
impl VersionType {
|
|
pub fn valid(&self) -> bool {
|
|
matches!(*self, VersionType::Object | VersionType::Delete)
|
|
}
|
|
|
|
pub fn to_u8(&self) -> u8 {
|
|
match self {
|
|
VersionType::Invalid => 0,
|
|
VersionType::Object => 1,
|
|
VersionType::Delete => 2,
|
|
}
|
|
}
|
|
|
|
pub fn from_u8(n: u8) -> Self {
|
|
match n {
|
|
1 => VersionType::Object,
|
|
2 => VersionType::Delete,
|
|
_ => VersionType::Invalid,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Default, Clone)]
|
|
pub enum ErasureAlgo {
|
|
#[default]
|
|
Invalid = 0,
|
|
ReedSolomon = 1,
|
|
}
|
|
|
|
impl ErasureAlgo {
|
|
pub fn valid(&self) -> bool {
|
|
*self > ErasureAlgo::Invalid
|
|
}
|
|
pub fn to_u8(&self) -> u8 {
|
|
match self {
|
|
ErasureAlgo::Invalid => 0,
|
|
ErasureAlgo::ReedSolomon => 1,
|
|
}
|
|
}
|
|
|
|
pub fn from_u8(u: u8) -> Self {
|
|
match u {
|
|
1 => ErasureAlgo::ReedSolomon,
|
|
_ => ErasureAlgo::Invalid,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for ErasureAlgo {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
ErasureAlgo::Invalid => write!(f, "Invalid"),
|
|
ErasureAlgo::ReedSolomon => write!(f, "{}", ERASURE_ALGORITHM),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Default, Clone)]
|
|
pub enum ChecksumAlgo {
|
|
#[default]
|
|
Invalid = 0,
|
|
HighwayHash = 1,
|
|
}
|
|
|
|
impl ChecksumAlgo {
|
|
pub fn valid(&self) -> bool {
|
|
*self > ChecksumAlgo::Invalid
|
|
}
|
|
pub fn to_u8(&self) -> u8 {
|
|
match self {
|
|
ChecksumAlgo::Invalid => 0,
|
|
ChecksumAlgo::HighwayHash => 1,
|
|
}
|
|
}
|
|
pub fn from_u8(u: u8) -> Self {
|
|
match u {
|
|
1 => ChecksumAlgo::HighwayHash,
|
|
_ => ChecksumAlgo::Invalid,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Default, Clone)]
|
|
pub enum Flags {
|
|
#[default]
|
|
FreeVersion = 1 << 0,
|
|
UsesDataDir = 1 << 1,
|
|
InlineData = 1 << 2,
|
|
}
|
|
|
|
const FREE_VERSION_META_HEADER: &str = "free-version";
|
|
|
|
// mergeXLV2Versions
|
|
pub fn merge_file_meta_versions(
|
|
mut quorum: usize,
|
|
mut strict: bool,
|
|
requested_versions: usize,
|
|
versions: &[Vec<FileMetaShallowVersion>],
|
|
) -> Vec<FileMetaShallowVersion> {
|
|
if quorum == 0 {
|
|
quorum = 1;
|
|
}
|
|
|
|
if versions.len() < quorum || versions.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
if versions.len() == 1 {
|
|
return versions[0].clone();
|
|
}
|
|
|
|
if quorum == 1 {
|
|
strict = true;
|
|
}
|
|
|
|
let mut versions = versions.to_owned();
|
|
|
|
let mut n_versions = 0;
|
|
|
|
let mut merged = Vec::new();
|
|
loop {
|
|
let mut tops = Vec::new();
|
|
let mut top_sig = FileMetaVersionHeader::default();
|
|
let mut consistent = true;
|
|
for vers in versions.iter() {
|
|
if vers.is_empty() {
|
|
consistent = false;
|
|
continue;
|
|
}
|
|
if tops.is_empty() {
|
|
consistent = true;
|
|
top_sig = vers[0].header.clone();
|
|
} else {
|
|
consistent = consistent && vers[0].header == top_sig;
|
|
}
|
|
tops.push(vers[0].clone());
|
|
}
|
|
|
|
// check if done...
|
|
if tops.len() < quorum {
|
|
break;
|
|
}
|
|
|
|
let mut latest = FileMetaShallowVersion::default();
|
|
if consistent {
|
|
merged.push(tops[0].clone());
|
|
if tops[0].header.free_version() {
|
|
n_versions += 1;
|
|
}
|
|
} else {
|
|
let mut lastest_count = 0;
|
|
for (i, ver) in tops.iter().enumerate() {
|
|
if ver.header == latest.header {
|
|
lastest_count += 1;
|
|
continue;
|
|
}
|
|
|
|
if i == 0 || ver.header.sorts_before(&latest.header) {
|
|
if i == 0 || lastest_count == 0 {
|
|
lastest_count = 1;
|
|
} else if !strict && ver.header.matches_not_strict(&latest.header) {
|
|
lastest_count += 1;
|
|
} else {
|
|
lastest_count = 1;
|
|
}
|
|
latest = ver.clone();
|
|
continue;
|
|
}
|
|
|
|
// Mismatch, but older.
|
|
if lastest_count > 0 && !strict && ver.header.matches_not_strict(&latest.header) {
|
|
lastest_count += 1;
|
|
continue;
|
|
}
|
|
|
|
if lastest_count > 0 && ver.header.version_id == latest.header.version_id {
|
|
let mut x: HashMap<FileMetaVersionHeader, usize> = HashMap::new();
|
|
for a in tops.iter() {
|
|
if a.header.version_id != ver.header.version_id {
|
|
continue;
|
|
}
|
|
let mut a_clone = a.clone();
|
|
if !strict {
|
|
a_clone.header.signature = [0; 4];
|
|
}
|
|
*x.entry(a_clone.header).or_insert(1) += 1;
|
|
}
|
|
lastest_count = 0;
|
|
for (k, v) in x.iter() {
|
|
if *v < lastest_count {
|
|
continue;
|
|
}
|
|
if *v == lastest_count && latest.header.sorts_before(k) {
|
|
continue;
|
|
}
|
|
tops.iter().for_each(|a| {
|
|
let mut hdr = a.header.clone();
|
|
if !strict {
|
|
hdr.signature = [0; 4];
|
|
}
|
|
if hdr == *k {
|
|
latest = a.clone();
|
|
}
|
|
});
|
|
|
|
lastest_count = *v;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if lastest_count >= quorum {
|
|
if !latest.header.free_version() {
|
|
n_versions += 1;
|
|
}
|
|
merged.push(latest.clone());
|
|
}
|
|
}
|
|
|
|
// Remove from all streams up until latest modtime or if selected.
|
|
versions.iter_mut().for_each(|vers| {
|
|
// // Keep top entry (and remaining)...
|
|
let mut bre = false;
|
|
vers.retain(|ver| {
|
|
if bre {
|
|
return true;
|
|
}
|
|
if let Ordering::Greater = ver.header.mod_time.cmp(&latest.header.mod_time) {
|
|
bre = true;
|
|
return false;
|
|
}
|
|
if ver.header == latest.header {
|
|
bre = true;
|
|
return false;
|
|
}
|
|
if let Ordering::Equal = latest.header.version_id.cmp(&ver.header.version_id) {
|
|
bre = true;
|
|
return false;
|
|
}
|
|
for merged_v in merged.iter() {
|
|
if let Ordering::Equal = ver.header.version_id.cmp(&merged_v.header.version_id) {
|
|
bre = true;
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
});
|
|
});
|
|
if requested_versions > 0 && requested_versions == n_versions {
|
|
merged.append(&mut versions[0]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Sanity check. Enable if duplicates show up.
|
|
// todo
|
|
merged
|
|
}
|
|
|
|
pub async fn file_info_from_raw(ri: RawFileInfo, bucket: &str, object: &str, read_data: bool) -> Result<FileInfo> {
|
|
get_file_info(&ri.buf, bucket, object, "", FileInfoOpts { data: read_data }).await
|
|
}
|
|
|
|
pub struct FileInfoOpts {
|
|
pub data: bool,
|
|
}
|
|
|
|
pub async fn get_file_info(buf: &[u8], volume: &str, path: &str, version_id: &str, opts: FileInfoOpts) -> Result<FileInfo> {
|
|
let vid = {
|
|
if version_id.is_empty() {
|
|
None
|
|
} else {
|
|
Some(Uuid::parse_str(version_id)?)
|
|
}
|
|
};
|
|
|
|
let meta = FileMeta::load(buf)?;
|
|
if meta.versions.is_empty() {
|
|
return Ok(FileInfo {
|
|
volume: volume.to_owned(),
|
|
name: path.to_owned(),
|
|
version_id: vid,
|
|
is_latest: true,
|
|
deleted: true,
|
|
mod_time: Some(OffsetDateTime::from_unix_timestamp(1)?),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
let fi = meta.into_fileinfo(volume, path, version_id, opts.data, true)?;
|
|
Ok(fi)
|
|
}
|
|
|
|
async fn read_more<R: AsyncRead + Unpin>(
|
|
reader: &mut R,
|
|
buf: &mut Vec<u8>,
|
|
total_size: usize,
|
|
read_size: usize,
|
|
has_full: bool,
|
|
) -> Result<()> {
|
|
use tokio::io::AsyncReadExt;
|
|
let has = buf.len();
|
|
|
|
if has >= read_size {
|
|
return Ok(());
|
|
}
|
|
|
|
if has_full || read_size > total_size {
|
|
return Err(Error::new(io::Error::new(io::ErrorKind::UnexpectedEof, "Unexpected EOF")));
|
|
}
|
|
|
|
let extra = read_size - has;
|
|
if buf.capacity() >= read_size {
|
|
// Extend the buffer if we have enough space.
|
|
buf.resize(read_size, 0);
|
|
} else {
|
|
buf.extend(vec![0u8; extra]);
|
|
}
|
|
|
|
reader.read_exact(&mut buf[has..]).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn read_xl_meta_no_data<R: AsyncRead + Unpin>(reader: &mut R, size: usize) -> Result<Vec<u8>> {
|
|
use tokio::io::AsyncReadExt;
|
|
|
|
let mut initial = size;
|
|
let mut has_full = true;
|
|
|
|
if initial > META_DATA_READ_DEFAULT {
|
|
initial = META_DATA_READ_DEFAULT;
|
|
has_full = false;
|
|
}
|
|
|
|
let mut buf = vec![0u8; initial];
|
|
reader.read_exact(&mut buf).await?;
|
|
|
|
let (tmp_buf, major, minor) = FileMeta::check_xl2_v1(&buf)?;
|
|
|
|
match major {
|
|
1 => match minor {
|
|
0 => {
|
|
read_more(reader, &mut buf, size, size, has_full).await?;
|
|
Ok(buf)
|
|
}
|
|
1..=3 => {
|
|
let (sz, tmp_buf) = FileMeta::read_bytes_header(tmp_buf)?;
|
|
let mut want = sz as usize + (buf.len() - tmp_buf.len());
|
|
|
|
if minor < 2 {
|
|
read_more(reader, &mut buf, size, want, has_full).await?;
|
|
return Ok(buf[..want].to_vec());
|
|
}
|
|
|
|
let want_max = usize::min(want + MSGP_UINT32_SIZE, size);
|
|
read_more(reader, &mut buf, size, want_max, has_full).await?;
|
|
|
|
if buf.len() < want {
|
|
error!("read_xl_meta_no_data buffer too small (length: {}, needed: {})", &buf.len(), want);
|
|
return Err(Error::new(DiskError::FileCorrupt));
|
|
}
|
|
|
|
let tmp = &buf[want..];
|
|
let crc_size = 5;
|
|
let other_size = tmp.len() - crc_size;
|
|
|
|
want += tmp.len() - other_size;
|
|
|
|
Ok(buf[..want].to_vec())
|
|
}
|
|
_ => Err(Error::new(io::Error::new(io::ErrorKind::InvalidData, "Unknown minor metadata version"))),
|
|
},
|
|
_ => Err(Error::new(io::Error::new(io::ErrorKind::InvalidData, "Unknown major metadata version"))),
|
|
}
|
|
}
|
|
#[cfg(test)]
|
|
#[allow(clippy::field_reassign_with_default)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_new_file_meta() {
|
|
let mut fm = FileMeta::new();
|
|
|
|
let (m, n) = (3, 2);
|
|
|
|
for i in 0..5 {
|
|
let mut fi = FileInfo::new(i.to_string().as_str(), m, n);
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
|
|
fm.add_version(fi).unwrap();
|
|
}
|
|
|
|
let buff = fm.marshal_msg().unwrap();
|
|
|
|
let mut newfm = FileMeta::default();
|
|
newfm.unmarshal_msg(&buff).unwrap();
|
|
|
|
assert_eq!(fm, newfm)
|
|
}
|
|
|
|
#[test]
|
|
fn test_marshal_metaobject() {
|
|
let obj = MetaObject {
|
|
data_dir: Some(Uuid::new_v4()),
|
|
..Default::default()
|
|
};
|
|
|
|
// println!("obj {:?}", &obj);
|
|
|
|
let encoded = obj.marshal_msg().unwrap();
|
|
|
|
let mut obj2 = MetaObject::default();
|
|
obj2.unmarshal_msg(&encoded).unwrap();
|
|
|
|
// println!("obj2 {:?}", &obj2);
|
|
|
|
assert_eq!(obj, obj2);
|
|
assert_eq!(obj.data_dir, obj2.data_dir);
|
|
}
|
|
|
|
#[test]
|
|
fn test_marshal_metadeletemarker() {
|
|
let obj = MetaDeleteMarker {
|
|
version_id: Some(Uuid::new_v4()),
|
|
..Default::default()
|
|
};
|
|
|
|
// println!("obj {:?}", &obj);
|
|
|
|
let encoded = obj.marshal_msg().unwrap();
|
|
|
|
let mut obj2 = MetaDeleteMarker::default();
|
|
obj2.unmarshal_msg(&encoded).unwrap();
|
|
|
|
// println!("obj2 {:?}", &obj2);
|
|
|
|
assert_eq!(obj, obj2);
|
|
assert_eq!(obj.version_id, obj2.version_id);
|
|
}
|
|
|
|
#[test]
|
|
#[tracing::instrument]
|
|
fn test_marshal_metaversion() {
|
|
let mut fi = FileInfo::new("test", 3, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
fi.mod_time = Some(OffsetDateTime::from_unix_timestamp(OffsetDateTime::now_utc().unix_timestamp()).unwrap());
|
|
let mut obj = FileMetaVersion::from(fi);
|
|
obj.write_version = 110;
|
|
|
|
// println!("obj {:?}", &obj);
|
|
|
|
let encoded = obj.marshal_msg().unwrap();
|
|
|
|
let mut obj2 = FileMetaVersion::default();
|
|
obj2.unmarshal_msg(&encoded).unwrap();
|
|
|
|
// println!("obj2 {:?}", &obj2);
|
|
|
|
// 时间截不一致 - -
|
|
assert_eq!(obj, obj2);
|
|
assert_eq!(obj.get_version_id(), obj2.get_version_id());
|
|
assert_eq!(obj.write_version, obj2.write_version);
|
|
assert_eq!(obj.write_version, 110);
|
|
}
|
|
|
|
#[test]
|
|
#[tracing::instrument]
|
|
fn test_marshal_metaversionheader() {
|
|
let mut obj = FileMetaVersionHeader::default();
|
|
let vid = Some(Uuid::new_v4());
|
|
obj.version_id = vid;
|
|
|
|
let encoded = obj.marshal_msg().unwrap();
|
|
|
|
let mut obj2 = FileMetaVersionHeader::default();
|
|
obj2.unmarshal_msg(&encoded).unwrap();
|
|
|
|
// 时间截不一致 - -
|
|
assert_eq!(obj, obj2);
|
|
assert_eq!(obj.version_id, obj2.version_id);
|
|
assert_eq!(obj.version_id, vid);
|
|
}
|
|
|
|
// New comprehensive tests for utility functions and validation
|
|
|
|
#[test]
|
|
fn test_xl_file_header_constants() {
|
|
// Test XL file header constants
|
|
assert_eq!(XL_FILE_HEADER, [b'X', b'L', b'2', b' ']);
|
|
assert_eq!(XL_FILE_VERSION_MAJOR, 1);
|
|
assert_eq!(XL_FILE_VERSION_MINOR, 3);
|
|
assert_eq!(XL_HEADER_VERSION, 3);
|
|
assert_eq!(XL_META_VERSION, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_xl2_v1_format() {
|
|
// Test valid XL2 V1 format
|
|
let mut valid_buf = vec![0u8; 20];
|
|
valid_buf[0..4].copy_from_slice(&XL_FILE_HEADER);
|
|
byteorder::LittleEndian::write_u16(&mut valid_buf[4..6], 1);
|
|
byteorder::LittleEndian::write_u16(&mut valid_buf[6..8], 0);
|
|
|
|
assert!(FileMeta::is_xl2_v1_format(&valid_buf));
|
|
|
|
// Test invalid format - wrong header
|
|
let invalid_buf = vec![0u8; 20];
|
|
assert!(!FileMeta::is_xl2_v1_format(&invalid_buf));
|
|
|
|
// Test buffer too small
|
|
let small_buf = vec![0u8; 4];
|
|
assert!(!FileMeta::is_xl2_v1_format(&small_buf));
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_xl2_v1() {
|
|
// Test valid XL2 V1 check
|
|
let mut valid_buf = vec![0u8; 20];
|
|
valid_buf[0..4].copy_from_slice(&XL_FILE_HEADER);
|
|
byteorder::LittleEndian::write_u16(&mut valid_buf[4..6], 1);
|
|
byteorder::LittleEndian::write_u16(&mut valid_buf[6..8], 2);
|
|
|
|
let result = FileMeta::check_xl2_v1(&valid_buf);
|
|
assert!(result.is_ok());
|
|
let (remaining, major, minor) = result.unwrap();
|
|
assert_eq!(major, 1);
|
|
assert_eq!(minor, 2);
|
|
assert_eq!(remaining.len(), 12); // 20 - 8
|
|
|
|
// Test buffer too small
|
|
let small_buf = vec![0u8; 4];
|
|
assert!(FileMeta::check_xl2_v1(&small_buf).is_err());
|
|
|
|
// Test wrong header
|
|
let mut wrong_header = vec![0u8; 20];
|
|
wrong_header[0..4].copy_from_slice(b"ABCD");
|
|
assert!(FileMeta::check_xl2_v1(&wrong_header).is_err());
|
|
|
|
// Test version too high
|
|
let mut high_version = vec![0u8; 20];
|
|
high_version[0..4].copy_from_slice(&XL_FILE_HEADER);
|
|
byteorder::LittleEndian::write_u16(&mut high_version[4..6], 99);
|
|
byteorder::LittleEndian::write_u16(&mut high_version[6..8], 0);
|
|
assert!(FileMeta::check_xl2_v1(&high_version).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_version_type_enum() {
|
|
// Test VersionType enum methods
|
|
assert!(VersionType::Object.valid());
|
|
assert!(VersionType::Delete.valid());
|
|
assert!(!VersionType::Invalid.valid());
|
|
|
|
assert_eq!(VersionType::Object.to_u8(), 1);
|
|
assert_eq!(VersionType::Delete.to_u8(), 2);
|
|
assert_eq!(VersionType::Invalid.to_u8(), 0);
|
|
|
|
assert_eq!(VersionType::from_u8(1), VersionType::Object);
|
|
assert_eq!(VersionType::from_u8(2), VersionType::Delete);
|
|
assert_eq!(VersionType::from_u8(99), VersionType::Invalid);
|
|
}
|
|
|
|
#[test]
|
|
fn test_erasure_algo_enum() {
|
|
// Test ErasureAlgo enum methods
|
|
assert!(ErasureAlgo::ReedSolomon.valid());
|
|
assert!(!ErasureAlgo::Invalid.valid());
|
|
|
|
assert_eq!(ErasureAlgo::ReedSolomon.to_u8(), 1);
|
|
assert_eq!(ErasureAlgo::Invalid.to_u8(), 0);
|
|
|
|
assert_eq!(ErasureAlgo::from_u8(1), ErasureAlgo::ReedSolomon);
|
|
assert_eq!(ErasureAlgo::from_u8(99), ErasureAlgo::Invalid);
|
|
|
|
// Test Display trait
|
|
assert_eq!(format!("{}", ErasureAlgo::ReedSolomon), "rs-vandermonde");
|
|
assert_eq!(format!("{}", ErasureAlgo::Invalid), "Invalid");
|
|
}
|
|
|
|
#[test]
|
|
fn test_checksum_algo_enum() {
|
|
// Test ChecksumAlgo enum methods
|
|
assert!(ChecksumAlgo::HighwayHash.valid());
|
|
assert!(!ChecksumAlgo::Invalid.valid());
|
|
|
|
assert_eq!(ChecksumAlgo::HighwayHash.to_u8(), 1);
|
|
assert_eq!(ChecksumAlgo::Invalid.to_u8(), 0);
|
|
|
|
assert_eq!(ChecksumAlgo::from_u8(1), ChecksumAlgo::HighwayHash);
|
|
assert_eq!(ChecksumAlgo::from_u8(99), ChecksumAlgo::Invalid);
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_version_header_methods() {
|
|
let mut header = FileMetaVersionHeader {
|
|
ec_n: 4,
|
|
ec_m: 2,
|
|
flags: XL_FLAG_FREE_VERSION,
|
|
..Default::default()
|
|
};
|
|
|
|
// Test has_ec
|
|
assert!(header.has_ec());
|
|
|
|
// Test free_version
|
|
assert!(header.free_version());
|
|
|
|
// Test user_data_dir (should be false by default)
|
|
assert!(!header.user_data_dir());
|
|
|
|
// Test with different flags
|
|
header.flags = 0;
|
|
assert!(!header.free_version());
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_version_header_comparison() {
|
|
let mut header1 = FileMetaVersionHeader {
|
|
mod_time: Some(OffsetDateTime::from_unix_timestamp(1000).unwrap()),
|
|
version_id: Some(Uuid::new_v4()),
|
|
..Default::default()
|
|
};
|
|
|
|
let mut header2 = FileMetaVersionHeader {
|
|
mod_time: Some(OffsetDateTime::from_unix_timestamp(2000).unwrap()),
|
|
version_id: Some(Uuid::new_v4()),
|
|
..Default::default()
|
|
};
|
|
|
|
// Test sorts_before - header2 should sort before header1 (newer mod_time)
|
|
assert!(!header1.sorts_before(&header2));
|
|
assert!(header2.sorts_before(&header1));
|
|
|
|
// Test matches_not_strict
|
|
let header3 = header1.clone();
|
|
assert!(header1.matches_not_strict(&header3));
|
|
|
|
// Test matches_ec
|
|
header1.ec_n = 4;
|
|
header1.ec_m = 2;
|
|
header2.ec_n = 4;
|
|
header2.ec_m = 2;
|
|
assert!(header1.matches_ec(&header2));
|
|
|
|
header2.ec_n = 6;
|
|
assert!(!header1.matches_ec(&header2));
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_version_methods() {
|
|
// Test with object version
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
fi.data_dir = Some(Uuid::new_v4());
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
|
|
let version = FileMetaVersion::from(fi.clone());
|
|
|
|
assert!(version.valid());
|
|
assert_eq!(version.get_version_id(), fi.version_id);
|
|
assert_eq!(version.get_data_dir(), fi.data_dir);
|
|
assert_eq!(version.get_mod_time(), fi.mod_time);
|
|
assert!(!version.free_version());
|
|
|
|
// Test with delete marker
|
|
let mut delete_fi = FileInfo::new("test", 4, 2);
|
|
delete_fi.deleted = true;
|
|
delete_fi.version_id = Some(Uuid::new_v4());
|
|
delete_fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
|
|
let delete_version = FileMetaVersion::from(delete_fi);
|
|
assert!(delete_version.valid());
|
|
assert_eq!(delete_version.version_type, VersionType::Delete);
|
|
}
|
|
|
|
#[test]
|
|
fn test_meta_object_methods() {
|
|
let mut obj = MetaObject {
|
|
data_dir: Some(Uuid::new_v4()),
|
|
size: 1024,
|
|
..Default::default()
|
|
};
|
|
|
|
// Test use_data_dir
|
|
assert!(obj.use_data_dir());
|
|
|
|
obj.data_dir = None;
|
|
assert!(obj.use_data_dir()); // use_data_dir always returns true
|
|
|
|
// Test use_inlinedata (currently always returns false)
|
|
obj.size = 100; // Small size
|
|
assert!(!obj.use_inlinedata());
|
|
|
|
obj.size = 100000; // Large size
|
|
assert!(!obj.use_inlinedata());
|
|
}
|
|
|
|
#[test]
|
|
fn test_meta_delete_marker_methods() {
|
|
let marker = MetaDeleteMarker::default();
|
|
|
|
// Test free_version (should always return false for delete markers)
|
|
assert!(!marker.free_version());
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_latest_mod_time() {
|
|
let mut fm = FileMeta::new();
|
|
|
|
// Empty FileMeta should return None
|
|
assert!(fm.lastest_mod_time().is_none());
|
|
|
|
// Add versions with different mod times
|
|
let time1 = OffsetDateTime::from_unix_timestamp(1000).unwrap();
|
|
let time2 = OffsetDateTime::from_unix_timestamp(2000).unwrap();
|
|
let time3 = OffsetDateTime::from_unix_timestamp(1500).unwrap();
|
|
|
|
let mut fi1 = FileInfo::new("test1", 4, 2);
|
|
fi1.mod_time = Some(time1);
|
|
fm.add_version(fi1).unwrap();
|
|
|
|
let mut fi2 = FileInfo::new("test2", 4, 2);
|
|
fi2.mod_time = Some(time2);
|
|
fm.add_version(fi2).unwrap();
|
|
|
|
let mut fi3 = FileInfo::new("test3", 4, 2);
|
|
fi3.mod_time = Some(time3);
|
|
fm.add_version(fi3).unwrap();
|
|
|
|
// Sort first to ensure latest is at the front
|
|
fm.sort_by_mod_time();
|
|
|
|
// Should return the first version's mod time (lastest_mod_time returns first version's time)
|
|
assert_eq!(fm.lastest_mod_time(), fm.versions[0].header.mod_time);
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_shard_data_dir_count() {
|
|
let mut fm = FileMeta::new();
|
|
let data_dir = Some(Uuid::new_v4());
|
|
|
|
// Add versions with same data_dir
|
|
for i in 0..3 {
|
|
let mut fi = FileInfo::new(&format!("test{}", i), 4, 2);
|
|
fi.data_dir = data_dir;
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi).unwrap();
|
|
}
|
|
|
|
// Add one version with different data_dir
|
|
let mut fi_diff = FileInfo::new("test_diff", 4, 2);
|
|
fi_diff.data_dir = Some(Uuid::new_v4());
|
|
fi_diff.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi_diff).unwrap();
|
|
|
|
// Count should be 0 because user_data_dir() requires UsesDataDir flag to be set
|
|
assert_eq!(fm.shard_data_dir_count(&None, &data_dir), 0);
|
|
|
|
// Count should be 0 for non-existent data_dir
|
|
assert_eq!(fm.shard_data_dir_count(&None, &Some(Uuid::new_v4())), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_sort_by_mod_time() {
|
|
let mut fm = FileMeta::new();
|
|
|
|
let time1 = OffsetDateTime::from_unix_timestamp(3000).unwrap();
|
|
let time2 = OffsetDateTime::from_unix_timestamp(1000).unwrap();
|
|
let time3 = OffsetDateTime::from_unix_timestamp(2000).unwrap();
|
|
|
|
// Add versions in non-chronological order
|
|
let mut fi1 = FileInfo::new("test1", 4, 2);
|
|
fi1.mod_time = Some(time1);
|
|
fm.add_version(fi1).unwrap();
|
|
|
|
let mut fi2 = FileInfo::new("test2", 4, 2);
|
|
fi2.mod_time = Some(time2);
|
|
fm.add_version(fi2).unwrap();
|
|
|
|
let mut fi3 = FileInfo::new("test3", 4, 2);
|
|
fi3.mod_time = Some(time3);
|
|
fm.add_version(fi3).unwrap();
|
|
|
|
// Sort by mod time
|
|
fm.sort_by_mod_time();
|
|
|
|
// Verify they are sorted (newest first) - add_version already sorts by insertion
|
|
// The actual order depends on how add_version inserts them
|
|
// Let's check the first version is the latest
|
|
let latest_time = fm.versions.iter().map(|v| v.header.mod_time).max().flatten();
|
|
assert_eq!(fm.versions[0].header.mod_time, latest_time);
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_find_version() {
|
|
let mut fm = FileMeta::new();
|
|
let version_id = Some(Uuid::new_v4());
|
|
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = version_id;
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi).unwrap();
|
|
|
|
// Should find the version
|
|
let result = fm.find_version(version_id);
|
|
assert!(result.is_ok());
|
|
let (idx, version) = result.unwrap();
|
|
assert_eq!(idx, 0);
|
|
assert_eq!(version.get_version_id(), version_id);
|
|
|
|
// Should not find non-existent version
|
|
let non_existent_id = Some(Uuid::new_v4());
|
|
assert!(fm.find_version(non_existent_id).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_delete_version() {
|
|
let mut fm = FileMeta::new();
|
|
let version_id = Some(Uuid::new_v4());
|
|
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = version_id;
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi.clone()).unwrap();
|
|
|
|
assert_eq!(fm.versions.len(), 1);
|
|
|
|
// Delete the version
|
|
let result = fm.delete_version(&fi);
|
|
assert!(result.is_ok());
|
|
|
|
// Version should be removed
|
|
assert_eq!(fm.versions.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_update_object_version() {
|
|
let mut fm = FileMeta::new();
|
|
let version_id = Some(Uuid::new_v4());
|
|
|
|
// Add initial version
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = version_id;
|
|
fi.size = 1024;
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi.clone()).unwrap();
|
|
|
|
// Update with new metadata (size is not updated by update_object_version)
|
|
let mut metadata = HashMap::new();
|
|
metadata.insert("test-key".to_string(), "test-value".to_string());
|
|
fi.metadata = Some(metadata.clone());
|
|
let result = fm.update_object_version(fi);
|
|
assert!(result.is_ok());
|
|
|
|
// Verify the metadata was updated
|
|
let (_, updated_version) = fm.find_version(version_id).unwrap();
|
|
if let Some(obj) = updated_version.object {
|
|
assert_eq!(obj.size, 1024); // Size remains unchanged
|
|
assert_eq!(obj.meta_user, Some(metadata)); // Metadata is updated
|
|
} else {
|
|
panic!("Expected object version");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_info_opts() {
|
|
let opts = FileInfoOpts { data: true };
|
|
assert!(opts.data);
|
|
|
|
let opts_no_data = FileInfoOpts { data: false };
|
|
assert!(!opts_no_data.data);
|
|
}
|
|
|
|
#[test]
|
|
fn test_decode_data_dir_from_meta() {
|
|
// Test with valid metadata containing data_dir
|
|
let data_dir = Some(Uuid::new_v4());
|
|
let obj = MetaObject {
|
|
data_dir,
|
|
mod_time: Some(OffsetDateTime::now_utc()),
|
|
erasure_algorithm: ErasureAlgo::ReedSolomon,
|
|
bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
|
|
..Default::default()
|
|
};
|
|
|
|
// Create a valid FileMetaVersion with the object
|
|
let version = FileMetaVersion {
|
|
version_type: VersionType::Object,
|
|
object: Some(obj),
|
|
..Default::default()
|
|
};
|
|
|
|
let encoded = version.marshal_msg().unwrap();
|
|
let result = FileMetaVersion::decode_data_dir_from_meta(&encoded);
|
|
assert!(result.is_ok());
|
|
assert_eq!(result.unwrap(), data_dir);
|
|
|
|
// Test with invalid metadata
|
|
let invalid_data = vec![0u8; 10];
|
|
let result = FileMetaVersion::decode_data_dir_from_meta(&invalid_data);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_latest_delete_marker() {
|
|
// Test the is_latest_delete_marker function with simple data
|
|
// Since the function is complex and requires specific XL format,
|
|
// we'll test with empty data which should return false
|
|
let empty_data = vec![];
|
|
assert!(!FileMeta::is_latest_delete_marker(&empty_data));
|
|
|
|
// Test with invalid data
|
|
let invalid_data = vec![1, 2, 3, 4, 5];
|
|
assert!(!FileMeta::is_latest_delete_marker(&invalid_data));
|
|
}
|
|
|
|
#[test]
|
|
fn test_merge_file_meta_versions_basic() {
|
|
// Test basic merge functionality
|
|
let mut version1 = FileMetaShallowVersion::default();
|
|
version1.header.version_id = Some(Uuid::new_v4());
|
|
version1.header.mod_time = Some(OffsetDateTime::from_unix_timestamp(1000).unwrap());
|
|
|
|
let mut version2 = FileMetaShallowVersion::default();
|
|
version2.header.version_id = Some(Uuid::new_v4());
|
|
version2.header.mod_time = Some(OffsetDateTime::from_unix_timestamp(2000).unwrap());
|
|
|
|
let versions = vec![
|
|
vec![version1.clone(), version2.clone()],
|
|
vec![version1.clone()],
|
|
vec![version2.clone()],
|
|
];
|
|
|
|
let merged = merge_file_meta_versions(2, false, 10, &versions);
|
|
|
|
// Should return versions that appear in at least quorum (2) sources
|
|
assert!(!merged.is_empty());
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_read_xl_meta_no_data() {
|
|
use tokio::fs;
|
|
use tokio::fs::File;
|
|
use tokio::io::AsyncWriteExt;
|
|
|
|
let mut fm = FileMeta::new();
|
|
|
|
let (m, n) = (3, 2);
|
|
|
|
for i in 0..5 {
|
|
let mut fi = FileInfo::new(i.to_string().as_str(), m, n);
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
|
|
fm.add_version(fi).unwrap();
|
|
}
|
|
|
|
// Use marshal_msg to create properly formatted data with XL headers
|
|
let buff = fm.marshal_msg().unwrap();
|
|
|
|
let filepath = "./test_xl.meta";
|
|
|
|
let mut file = File::create(filepath).await.unwrap();
|
|
file.write_all(&buff).await.unwrap();
|
|
|
|
let mut f = File::open(filepath).await.unwrap();
|
|
|
|
let stat = f.metadata().await.unwrap();
|
|
|
|
let data = read_xl_meta_no_data(&mut f, stat.len() as usize).await.unwrap();
|
|
|
|
let mut newfm = FileMeta::default();
|
|
newfm.unmarshal_msg(&data).unwrap();
|
|
|
|
fs::remove_file(filepath).await.unwrap();
|
|
|
|
assert_eq!(fm, newfm)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_file_info() {
|
|
// Test get_file_info function
|
|
let mut fm = FileMeta::new();
|
|
let version_id = Uuid::new_v4();
|
|
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(version_id);
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi).unwrap();
|
|
|
|
let encoded = fm.marshal_msg().unwrap();
|
|
|
|
let opts = FileInfoOpts { data: false };
|
|
let result = get_file_info(&encoded, "test-volume", "test-path", &version_id.to_string(), opts).await;
|
|
|
|
assert!(result.is_ok());
|
|
let file_info = result.unwrap();
|
|
assert_eq!(file_info.volume, "test-volume");
|
|
assert_eq!(file_info.name, "test-path");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_file_info_from_raw() {
|
|
// Test file_info_from_raw function
|
|
let mut fm = FileMeta::new();
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi).unwrap();
|
|
|
|
let encoded = fm.marshal_msg().unwrap();
|
|
|
|
let raw_info = RawFileInfo { buf: encoded };
|
|
|
|
let result = file_info_from_raw(raw_info, "test-bucket", "test-object", false).await;
|
|
assert!(result.is_ok());
|
|
|
|
let file_info = result.unwrap();
|
|
assert_eq!(file_info.volume, "test-bucket");
|
|
assert_eq!(file_info.name, "test-object");
|
|
}
|
|
|
|
// Additional comprehensive tests for better coverage
|
|
|
|
#[test]
|
|
fn test_file_meta_load_function() {
|
|
// Test FileMeta::load function
|
|
let mut fm = FileMeta::new();
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi).unwrap();
|
|
|
|
let encoded = fm.marshal_msg().unwrap();
|
|
|
|
// Test successful load
|
|
let loaded_fm = FileMeta::load(&encoded);
|
|
assert!(loaded_fm.is_ok());
|
|
assert_eq!(loaded_fm.unwrap(), fm);
|
|
|
|
// Test load with invalid data
|
|
let invalid_data = vec![0u8; 10];
|
|
let result = FileMeta::load(&invalid_data);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_read_bytes_header() {
|
|
// Create a real FileMeta and marshal it to get proper format
|
|
let mut fm = FileMeta::new();
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi).unwrap();
|
|
|
|
let marshaled = fm.marshal_msg().unwrap();
|
|
|
|
// First call check_xl2_v1 to get the buffer after XL header validation
|
|
let (after_xl_header, _major, _minor) = FileMeta::check_xl2_v1(&marshaled).unwrap();
|
|
|
|
// Ensure we have at least 5 bytes for read_bytes_header
|
|
if after_xl_header.len() < 5 {
|
|
panic!("Buffer too small: {} bytes, need at least 5", after_xl_header.len());
|
|
}
|
|
|
|
// Now call read_bytes_header on the remaining buffer
|
|
let result = FileMeta::read_bytes_header(after_xl_header);
|
|
assert!(result.is_ok());
|
|
let (length, remaining) = result.unwrap();
|
|
|
|
// The length should be greater than 0 for real data
|
|
assert!(length > 0);
|
|
// remaining should be everything after the 5-byte header
|
|
assert_eq!(remaining.len(), after_xl_header.len() - 5);
|
|
|
|
// Test with buffer too small
|
|
let small_buf = vec![0u8; 2];
|
|
let result = FileMeta::read_bytes_header(&small_buf);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_get_set_idx() {
|
|
let mut fm = FileMeta::new();
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi).unwrap();
|
|
|
|
// Test get_idx
|
|
let result = fm.get_idx(0);
|
|
assert!(result.is_ok());
|
|
|
|
// Test get_idx with invalid index
|
|
let result = fm.get_idx(10);
|
|
assert!(result.is_err());
|
|
|
|
// Test set_idx
|
|
let new_version = FileMetaVersion {
|
|
version_type: VersionType::Object,
|
|
..Default::default()
|
|
};
|
|
let result = fm.set_idx(0, new_version);
|
|
assert!(result.is_ok());
|
|
|
|
// Test set_idx with invalid index
|
|
let invalid_version = FileMetaVersion::default();
|
|
let result = fm.set_idx(10, invalid_version);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_into_fileinfo() {
|
|
let mut fm = FileMeta::new();
|
|
let version_id = Uuid::new_v4();
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(version_id);
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi).unwrap();
|
|
|
|
// Test into_fileinfo with valid version_id
|
|
let result = fm.into_fileinfo("test-volume", "test-path", &version_id.to_string(), false, false);
|
|
assert!(result.is_ok());
|
|
let file_info = result.unwrap();
|
|
assert_eq!(file_info.volume, "test-volume");
|
|
assert_eq!(file_info.name, "test-path");
|
|
|
|
// Test into_fileinfo with invalid version_id
|
|
let invalid_id = Uuid::new_v4();
|
|
let result = fm.into_fileinfo("test-volume", "test-path", &invalid_id.to_string(), false, false);
|
|
assert!(result.is_err());
|
|
|
|
// Test into_fileinfo with empty version_id (should get latest)
|
|
let result = fm.into_fileinfo("test-volume", "test-path", "", false, false);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_into_file_info_versions() {
|
|
let mut fm = FileMeta::new();
|
|
|
|
// Add multiple versions
|
|
for i in 0..3 {
|
|
let mut fi = FileInfo::new(&format!("test{}", i), 4, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
fi.mod_time = Some(OffsetDateTime::from_unix_timestamp(1000 + i).unwrap());
|
|
fm.add_version(fi).unwrap();
|
|
}
|
|
|
|
let result = fm.into_file_info_versions("test-volume", "test-path", false);
|
|
assert!(result.is_ok());
|
|
let versions = result.unwrap();
|
|
assert_eq!(versions.versions.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_shallow_version_to_fileinfo() {
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
|
|
let version = FileMetaVersion::from(fi.clone());
|
|
let shallow_version = FileMetaShallowVersion::try_from(version).unwrap();
|
|
|
|
let result = shallow_version.to_fileinfo("test-volume", "test-path", fi.version_id, false);
|
|
assert!(result.is_ok());
|
|
let converted_fi = result.unwrap();
|
|
assert_eq!(converted_fi.volume, "test-volume");
|
|
assert_eq!(converted_fi.name, "test-path");
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_version_try_from_bytes() {
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
let version = FileMetaVersion::from(fi);
|
|
let encoded = version.marshal_msg().unwrap();
|
|
|
|
// Test successful conversion
|
|
let result = FileMetaVersion::try_from(encoded.as_slice());
|
|
assert!(result.is_ok());
|
|
|
|
// Test with invalid data
|
|
let invalid_data = vec![0u8; 5];
|
|
let result = FileMetaVersion::try_from(invalid_data.as_slice());
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_version_try_from_shallow() {
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
let version = FileMetaVersion::from(fi);
|
|
let shallow = FileMetaShallowVersion::try_from(version.clone()).unwrap();
|
|
|
|
let result = FileMetaVersion::try_from(shallow);
|
|
assert!(result.is_ok());
|
|
let converted = result.unwrap();
|
|
assert_eq!(converted.get_version_id(), version.get_version_id());
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_version_header_from_version() {
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
let version = FileMetaVersion::from(fi.clone());
|
|
|
|
let header = FileMetaVersionHeader::from(version);
|
|
assert_eq!(header.version_id, fi.version_id);
|
|
assert_eq!(header.mod_time, fi.mod_time);
|
|
}
|
|
|
|
#[test]
|
|
fn test_meta_object_into_fileinfo() {
|
|
let obj = MetaObject {
|
|
version_id: Some(Uuid::new_v4()),
|
|
size: 1024,
|
|
mod_time: Some(OffsetDateTime::now_utc()),
|
|
..Default::default()
|
|
};
|
|
|
|
let version_id = obj.version_id;
|
|
let expected_version_id = version_id;
|
|
let file_info = obj.into_fileinfo("test-volume", "test-path", version_id, false);
|
|
assert_eq!(file_info.volume, "test-volume");
|
|
assert_eq!(file_info.name, "test-path");
|
|
assert_eq!(file_info.size, 1024);
|
|
assert_eq!(file_info.version_id, expected_version_id);
|
|
}
|
|
|
|
#[test]
|
|
fn test_meta_object_from_fileinfo() {
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
fi.data_dir = Some(Uuid::new_v4());
|
|
fi.size = 2048;
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
|
|
let obj = MetaObject::from(fi.clone());
|
|
assert_eq!(obj.version_id, fi.version_id);
|
|
assert_eq!(obj.data_dir, fi.data_dir);
|
|
assert_eq!(obj.size, fi.size);
|
|
assert_eq!(obj.mod_time, fi.mod_time);
|
|
}
|
|
|
|
#[test]
|
|
fn test_meta_delete_marker_into_fileinfo() {
|
|
let marker = MetaDeleteMarker {
|
|
version_id: Some(Uuid::new_v4()),
|
|
mod_time: Some(OffsetDateTime::now_utc()),
|
|
..Default::default()
|
|
};
|
|
|
|
let version_id = marker.version_id;
|
|
let expected_version_id = version_id;
|
|
let file_info = marker.into_fileinfo("test-volume", "test-path", version_id, false);
|
|
assert_eq!(file_info.volume, "test-volume");
|
|
assert_eq!(file_info.name, "test-path");
|
|
assert_eq!(file_info.version_id, expected_version_id);
|
|
assert!(file_info.deleted);
|
|
}
|
|
|
|
#[test]
|
|
fn test_meta_delete_marker_from_fileinfo() {
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fi.deleted = true;
|
|
|
|
let marker = MetaDeleteMarker::from(fi.clone());
|
|
assert_eq!(marker.version_id, fi.version_id);
|
|
assert_eq!(marker.mod_time, fi.mod_time);
|
|
}
|
|
|
|
#[test]
|
|
fn test_flags_enum() {
|
|
// Test Flags enum values
|
|
assert_eq!(Flags::FreeVersion as u8, 1);
|
|
assert_eq!(Flags::UsesDataDir as u8, 2);
|
|
assert_eq!(Flags::InlineData as u8, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_version_header_user_data_dir() {
|
|
let header = FileMetaVersionHeader {
|
|
flags: 0,
|
|
..Default::default()
|
|
};
|
|
|
|
// Test without UsesDataDir flag
|
|
assert!(!header.user_data_dir());
|
|
|
|
// Test with UsesDataDir flag
|
|
let header = FileMetaVersionHeader {
|
|
flags: Flags::UsesDataDir as u8,
|
|
..Default::default()
|
|
};
|
|
assert!(header.user_data_dir());
|
|
|
|
// Test with multiple flags including UsesDataDir
|
|
let header = FileMetaVersionHeader {
|
|
flags: Flags::UsesDataDir as u8 | Flags::FreeVersion as u8,
|
|
..Default::default()
|
|
};
|
|
assert!(header.user_data_dir());
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_version_header_ordering() {
|
|
let header1 = FileMetaVersionHeader {
|
|
mod_time: Some(OffsetDateTime::from_unix_timestamp(1000).unwrap()),
|
|
version_id: Some(Uuid::new_v4()),
|
|
..Default::default()
|
|
};
|
|
|
|
let header2 = FileMetaVersionHeader {
|
|
mod_time: Some(OffsetDateTime::from_unix_timestamp(2000).unwrap()),
|
|
version_id: Some(Uuid::new_v4()),
|
|
..Default::default()
|
|
};
|
|
|
|
// Test partial_cmp
|
|
assert!(header1.partial_cmp(&header2).is_some());
|
|
|
|
// Test cmp - header2 should be greater (newer)
|
|
use std::cmp::Ordering;
|
|
assert_eq!(header1.cmp(&header2), Ordering::Less); // header1 has earlier time
|
|
assert_eq!(header2.cmp(&header1), Ordering::Greater); // header2 has later time
|
|
assert_eq!(header1.cmp(&header1), Ordering::Equal);
|
|
}
|
|
|
|
#[test]
|
|
fn test_merge_file_meta_versions_edge_cases() {
|
|
// Test with empty versions
|
|
let empty_versions: Vec<Vec<FileMetaShallowVersion>> = vec![];
|
|
let merged = merge_file_meta_versions(1, false, 10, &empty_versions);
|
|
assert!(merged.is_empty());
|
|
|
|
// Test with quorum larger than available sources
|
|
let mut version = FileMetaShallowVersion::default();
|
|
version.header.version_id = Some(Uuid::new_v4());
|
|
let versions = vec![vec![version]];
|
|
let merged = merge_file_meta_versions(5, false, 10, &versions);
|
|
assert!(merged.is_empty());
|
|
|
|
// Test strict mode
|
|
let mut version1 = FileMetaShallowVersion::default();
|
|
version1.header.version_id = Some(Uuid::new_v4());
|
|
version1.header.mod_time = Some(OffsetDateTime::from_unix_timestamp(1000).unwrap());
|
|
|
|
let mut version2 = FileMetaShallowVersion::default();
|
|
version2.header.version_id = Some(Uuid::new_v4());
|
|
version2.header.mod_time = Some(OffsetDateTime::from_unix_timestamp(2000).unwrap());
|
|
|
|
let versions = vec![vec![version1.clone()], vec![version2.clone()]];
|
|
|
|
let _merged_strict = merge_file_meta_versions(1, true, 10, &versions);
|
|
let merged_non_strict = merge_file_meta_versions(1, false, 10, &versions);
|
|
|
|
// In strict mode, behavior might be different
|
|
assert!(!merged_non_strict.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_read_more_function() {
|
|
use std::io::Cursor;
|
|
|
|
let data = b"Hello, World! This is test data.";
|
|
let mut reader = Cursor::new(data);
|
|
let mut buf = vec![0u8; 10];
|
|
|
|
// Test reading more data
|
|
let result = read_more(&mut reader, &mut buf, 33, 20, false).await;
|
|
assert!(result.is_ok());
|
|
assert_eq!(buf.len(), 20);
|
|
|
|
// Test with has_full = true and buffer already has enough data
|
|
let mut reader2 = Cursor::new(data);
|
|
let mut buf2 = vec![0u8; 5];
|
|
let result = read_more(&mut reader2, &mut buf2, 10, 5, true).await;
|
|
assert!(result.is_ok());
|
|
assert_eq!(buf2.len(), 5); // Should remain 5 since has >= read_size
|
|
|
|
// Test reading beyond available data
|
|
let mut reader3 = Cursor::new(b"short");
|
|
let mut buf3 = vec![0u8; 2];
|
|
let result = read_more(&mut reader3, &mut buf3, 100, 98, false).await;
|
|
// Should handle gracefully even if not enough data
|
|
assert!(result.is_ok() || result.is_err()); // Either is acceptable
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_read_xl_meta_no_data_edge_cases() {
|
|
use std::io::Cursor;
|
|
|
|
// Test with empty data
|
|
let empty_data = vec![];
|
|
let mut reader = Cursor::new(empty_data);
|
|
let result = read_xl_meta_no_data(&mut reader, 0).await;
|
|
assert!(result.is_err()); // Should fail because buffer is empty
|
|
|
|
// Test with very small size (should fail because it's not valid XL format)
|
|
let small_data = vec![1, 2, 3];
|
|
let mut reader = Cursor::new(small_data);
|
|
let result = read_xl_meta_no_data(&mut reader, 3).await;
|
|
assert!(result.is_err()); // Should fail because data is too small for XL format
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_file_info_edge_cases() {
|
|
// Test with empty buffer
|
|
let empty_buf = vec![];
|
|
let opts = FileInfoOpts { data: false };
|
|
let result = get_file_info(&empty_buf, "volume", "path", "version", opts).await;
|
|
assert!(result.is_err());
|
|
|
|
// Test with invalid version_id format
|
|
let mut fm = FileMeta::new();
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi).unwrap();
|
|
let encoded = fm.marshal_msg().unwrap();
|
|
|
|
let opts = FileInfoOpts { data: false };
|
|
let result = get_file_info(&encoded, "volume", "path", "invalid-uuid", opts).await;
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_file_info_from_raw_edge_cases() {
|
|
// Test with empty buffer
|
|
let empty_raw = RawFileInfo { buf: vec![] };
|
|
let result = file_info_from_raw(empty_raw, "bucket", "object", false).await;
|
|
assert!(result.is_err());
|
|
|
|
// Test with invalid buffer
|
|
let invalid_raw = RawFileInfo {
|
|
buf: vec![1, 2, 3, 4, 5],
|
|
};
|
|
let result = file_info_from_raw(invalid_raw, "bucket", "object", false).await;
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_version_invalid_cases() {
|
|
// Test invalid version
|
|
let version = FileMetaVersion {
|
|
version_type: VersionType::Invalid,
|
|
..Default::default()
|
|
};
|
|
assert!(!version.valid());
|
|
|
|
// Test version with neither object nor delete marker
|
|
let version = FileMetaVersion {
|
|
version_type: VersionType::Object,
|
|
object: None,
|
|
delete_marker: None,
|
|
..Default::default()
|
|
};
|
|
assert!(!version.valid());
|
|
}
|
|
|
|
#[test]
|
|
fn test_meta_object_edge_cases() {
|
|
let obj = MetaObject {
|
|
data_dir: None,
|
|
..Default::default()
|
|
};
|
|
|
|
// Test use_data_dir with None (use_data_dir always returns true)
|
|
assert!(obj.use_data_dir());
|
|
|
|
// Test use_inlinedata (always returns false in current implementation)
|
|
let obj = MetaObject {
|
|
size: 128 * 1024, // 128KB threshold
|
|
..Default::default()
|
|
};
|
|
assert!(!obj.use_inlinedata()); // Should be false
|
|
|
|
let obj = MetaObject {
|
|
size: 128 * 1024 - 1,
|
|
..Default::default()
|
|
};
|
|
assert!(!obj.use_inlinedata()); // Should also be false (always false)
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_version_header_edge_cases() {
|
|
let header = FileMetaVersionHeader {
|
|
ec_n: 0,
|
|
ec_m: 0,
|
|
..Default::default()
|
|
};
|
|
|
|
// Test has_ec with zero values
|
|
assert!(!header.has_ec());
|
|
|
|
// Test matches_not_strict with different signatures but same version_id
|
|
let version_id = Some(Uuid::new_v4());
|
|
let header = FileMetaVersionHeader {
|
|
version_id,
|
|
version_type: VersionType::Object,
|
|
signature: [1, 2, 3, 4],
|
|
..Default::default()
|
|
};
|
|
let other = FileMetaVersionHeader {
|
|
version_id,
|
|
version_type: VersionType::Object,
|
|
signature: [5, 6, 7, 8],
|
|
..Default::default()
|
|
};
|
|
// Should match because they have same version_id and type
|
|
assert!(header.matches_not_strict(&other));
|
|
|
|
// Test sorts_before with same mod_time but different version_id
|
|
let time = OffsetDateTime::from_unix_timestamp(1000).unwrap();
|
|
let header_time1 = FileMetaVersionHeader {
|
|
mod_time: Some(time),
|
|
version_id: Some(Uuid::new_v4()),
|
|
..Default::default()
|
|
};
|
|
let header_time2 = FileMetaVersionHeader {
|
|
mod_time: Some(time),
|
|
version_id: Some(Uuid::new_v4()),
|
|
..Default::default()
|
|
};
|
|
|
|
// Should use version_id for comparison when mod_time is same
|
|
let sorts_before = header_time1.sorts_before(&header_time2);
|
|
assert!(sorts_before || header_time2.sorts_before(&header_time1)); // One should sort before the other
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_add_version_edge_cases() {
|
|
let mut fm = FileMeta::new();
|
|
|
|
// Test adding version with same version_id (should update)
|
|
let version_id = Some(Uuid::new_v4());
|
|
let mut fi1 = FileInfo::new("test1", 4, 2);
|
|
fi1.version_id = version_id;
|
|
fi1.size = 1024;
|
|
fi1.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi1).unwrap();
|
|
|
|
let mut fi2 = FileInfo::new("test2", 4, 2);
|
|
fi2.version_id = version_id;
|
|
fi2.size = 2048;
|
|
fi2.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi2).unwrap();
|
|
|
|
// Should still have only one version, but updated
|
|
assert_eq!(fm.versions.len(), 1);
|
|
let (_, version) = fm.find_version(version_id).unwrap();
|
|
if let Some(obj) = version.object {
|
|
assert_eq!(obj.size, 2048); // Size gets updated when adding same version_id
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_delete_version_edge_cases() {
|
|
let mut fm = FileMeta::new();
|
|
|
|
// Test deleting non-existent version
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = Some(Uuid::new_v4());
|
|
|
|
let result = fm.delete_version(&fi);
|
|
assert!(result.is_err()); // Should fail for non-existent version
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_meta_shard_data_dir_count_edge_cases() {
|
|
let mut fm = FileMeta::new();
|
|
|
|
// Test with None data_dir parameter
|
|
let count = fm.shard_data_dir_count(&None, &None);
|
|
assert_eq!(count, 0);
|
|
|
|
// Test with version_id parameter (not None)
|
|
let version_id = Some(Uuid::new_v4());
|
|
let data_dir = Some(Uuid::new_v4());
|
|
|
|
let mut fi = FileInfo::new("test", 4, 2);
|
|
fi.version_id = version_id;
|
|
fi.data_dir = data_dir;
|
|
fi.mod_time = Some(OffsetDateTime::now_utc());
|
|
fm.add_version(fi).unwrap();
|
|
|
|
let count = fm.shard_data_dir_count(&version_id, &data_dir);
|
|
assert_eq!(count, 0); // Should be 0 because user_data_dir() requires flag
|
|
|
|
// Test with different version_id
|
|
let other_version_id = Some(Uuid::new_v4());
|
|
let count = fm.shard_data_dir_count(&other_version_id, &data_dir);
|
|
assert_eq!(count, 1); // Should be 1 because the version has matching data_dir and user_data_dir() is true
|
|
}
|