use super::sync_backend::{SyncBackend, SyncIndex, SyncManifest};
use crate::repository::backend::{
BackendError, Chunk, ChunkID, ChunkSettings, EncryptedKey, Result, SegmentDescriptor,
StoredArchive,
};
use crate::repository::Key;
use asuran_core::repository::backend::flatfile::{
EntryFooter, EntryFooterData, EntryHeader, FlatFileHeader,
};
use asuran_core::repository::chunk::{ChunkBody, ChunkHeader};
use chrono::{DateTime, FixedOffset};
use semver::Version;
use uuid::Uuid;
use std::collections::{HashMap, HashSet};
use std::convert::TryInto;
use std::fmt::Debug;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
pub use asuran_core::repository::backend::flatfile::MAGIC_NUMBER;
pub struct GenericFlatFile<F: Read + Write + Seek + 'static> {
file: F,
path: PathBuf,
chunk_settings: ChunkSettings,
index: HashMap<ChunkID, SegmentDescriptor>,
length_map: HashMap<SegmentDescriptor, u64>,
manifest: Vec<StoredArchive>,
entry_footer_data: EntryFooterData,
chunk_settings_modified: bool,
enc_key: EncryptedKey,
key: Key,
chunk_headers: HashMap<SegmentDescriptor, ChunkHeader>,
header_offset: u64,
seen_versions: HashSet<(Version, Uuid)>,
}
impl<F: Read + Write + Seek + 'static> Debug for GenericFlatFile<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GenericFlatFile")
.field("file_type", &std::any::type_name::<F>())
.field("path", &self.path)
.finish()
}
}
impl<F: Read + Write + Seek + 'static> GenericFlatFile<F> {
#[allow(clippy::too_many_lines)]
pub fn new_raw(
mut file: F,
path: impl AsRef<Path>,
settings: Option<ChunkSettings>,
key: Key,
enc_key: Option<EncryptedKey>,
) -> Result<GenericFlatFile<F>> {
let mut seen_versions = HashSet::new();
seen_versions.insert((crate::VERSION_STRUCT.clone(), *crate::IMPLEMENTATION_UUID));
let file_length = file.seek(SeekFrom::End(0))?;
if file_length == 0 {
let settings = settings.ok_or_else(|| {
BackendError::ManifestError(
"Attempted to create a FlatFile without supplying chunk settings".to_string(),
)
})?;
let enc_key = enc_key.ok_or_else(|| {
BackendError::ManifestError(
"Attempted to create a FlatFile without supplying an encrypted key".to_string(),
)
})?;
let header = FlatFileHeader::new(&enc_key)?;
header.to_write(&mut file)?;
let header =
EntryHeader::new(&*crate::VERSION_STRUCT, 0, 0, *crate::IMPLEMENTATION_UUID)?;
let header_location = file.seek(SeekFrom::End(0))?;
header.to_write(&mut file)?;
let flat_file = GenericFlatFile {
file,
path: path.as_ref().to_owned(),
chunk_settings: settings,
index: HashMap::new(),
length_map: HashMap::new(),
manifest: Vec::new(),
entry_footer_data: EntryFooterData::new(settings),
chunk_settings_modified: true,
enc_key,
key,
chunk_headers: HashMap::new(),
header_offset: header_location,
seen_versions,
};
Ok(flat_file)
} else {
let path: PathBuf = path.as_ref().to_owned();
file.seek(SeekFrom::Start(0))?;
let global_header = FlatFileHeader::from_read(&mut file)?;
if enc_key.is_some() {
return Err(BackendError::ManifestError(
"Attempted to set a key on an already existing flatfile repository".to_string(),
));
}
let enc_key = global_header.key()?;
let mut header_offset = file.seek(SeekFrom::Current(0))?;
let mut entry_header = EntryHeader::from_read(&mut file)?;
let mut chunk_settings: Option<ChunkSettings> = None;
let mut index = HashMap::new();
let mut length_map = HashMap::new();
let mut manifest = Vec::new();
let mut chunk_headers = HashMap::new();
while entry_header.footer_offset != 0 && entry_header.next_header_offset != 0 {
seen_versions.insert((entry_header.version(), entry_header.uuid()));
file.seek(SeekFrom::Start(entry_header.footer_offset))?;
let footer = EntryFooter::from_read(&mut file)?.into_data(&key)?;
chunk_settings = Some(footer.chunk_settings);
for (id, start, length) in footer.chunk_locations {
let descriptor = SegmentDescriptor {
segment_id: 0,
start,
};
index.insert(id, descriptor);
length_map.insert(descriptor, length);
let header = footer
.chunk_headers
.get(&id)
.ok_or_else(|| {
BackendError::IndexError(format!(
"Chunk with id {:?} did not have an associated header.",
id
))
})?
.clone();
chunk_headers.insert(descriptor, header);
}
for (id, timestamp) in footer.archives {
manifest.push(StoredArchive { id, timestamp });
}
header_offset = file.seek(SeekFrom::Start(entry_header.next_header_offset))?;
entry_header = EntryHeader::from_read(&mut file)?;
}
let chunk_settings = chunk_settings.ok_or_else(|| {
BackendError::ManifestError(format!(
"FlatFile repository at {:?} did not contain any valid entries",
path
))
})?;
let flat_file = GenericFlatFile {
file,
path,
chunk_settings,
index,
length_map,
manifest,
entry_footer_data: EntryFooterData::new(chunk_settings),
chunk_settings_modified: false,
enc_key,
key,
chunk_headers,
header_offset,
seen_versions,
};
Ok(flat_file)
}
}
pub fn load_encrypted_key(mut file: F) -> Result<EncryptedKey> {
file.seek(SeekFrom::Start(0))?;
let header = FlatFileHeader::from_read(&mut file)?;
Ok(header.key()?)
}
}
impl<F: Read + Write + Seek + 'static> SyncManifest for GenericFlatFile<F> {
type Iterator = std::vec::IntoIter<StoredArchive>;
fn last_modification(&mut self) -> Result<DateTime<FixedOffset>> {
if self.manifest.is_empty() {
Err(BackendError::ManifestError(
"No archives/timestamps present".to_string(),
))
} else {
let archive = &self.manifest[self.manifest.len() - 1];
Ok(archive.timestamp())
}
}
fn chunk_settings(&mut self) -> ChunkSettings {
self.chunk_settings
}
fn write_chunk_settings(&mut self, settings: ChunkSettings) -> Result<()> {
self.chunk_settings = settings;
self.entry_footer_data.chunk_settings = settings;
self.chunk_settings_modified = true;
Ok(())
}
fn archive_iterator(&mut self) -> Self::Iterator {
self.manifest.clone().into_iter()
}
fn write_archive(&mut self, archive: StoredArchive) -> Result<()> {
self.entry_footer_data
.add_archive(archive.id, archive.timestamp);
self.manifest.push(archive);
Ok(())
}
fn touch(&mut self) -> Result<()> {
Ok(())
}
fn seen_versions(&mut self) -> HashSet<(Version, Uuid)> {
self.seen_versions.clone()
}
}
impl<F: Read + Write + Seek + 'static> SyncIndex for GenericFlatFile<F> {
fn lookup_chunk(&mut self, id: ChunkID) -> Option<SegmentDescriptor> {
self.index.get(&id).copied()
}
fn set_chunk(&mut self, id: ChunkID, location: SegmentDescriptor) -> Result<()> {
let length = self.length_map.get(&location).ok_or_else(|| {
BackendError::IndexError(format!(
"Attempted to add chunk with id {:?} to the index, whose length was not known",
id
))
})?;
self.index.insert(id, location);
let location = location.start;
self.entry_footer_data.add_chunk(id, location, *length);
Ok(())
}
fn known_chunks(&mut self) -> HashSet<ChunkID> {
self.index.keys().copied().collect()
}
fn commit_index(&mut self) -> Result<()> {
if self.chunk_settings_modified || self.entry_footer_data.dirty() {
self.chunk_settings_modified = false;
let mut footer = EntryFooterData::new(self.chunk_settings);
std::mem::swap(&mut self.entry_footer_data, &mut footer);
let footer = EntryFooter::from_data(&footer, &self.key, self.chunk_settings);
let file = &mut self.file;
let footer_location = file.seek(SeekFrom::End(0))?;
footer.to_write(Write::by_ref(file))?;
let header_location = file.seek(SeekFrom::End(0))?;
EntryHeader::new(&*crate::VERSION_STRUCT, 0, 0, *crate::IMPLEMENTATION_UUID)?
.to_write(Write::by_ref(file))?;
file.seek(SeekFrom::Start(self.header_offset))?;
EntryHeader::new(
&*crate::VERSION_STRUCT,
footer_location,
header_location,
*crate::IMPLEMENTATION_UUID,
)?
.to_write(Write::by_ref(file))?;
self.header_offset = header_location;
Ok(())
} else {
Ok(())
}
}
fn chunk_count(&mut self) -> usize {
self.index.len()
}
}
impl<F: Read + Write + Seek + 'static> SyncBackend for GenericFlatFile<F> {
type SyncManifest = Self;
type SyncIndex = Self;
fn get_index(&mut self) -> &mut Self::SyncIndex {
self
}
fn get_manifest(&mut self) -> &mut Self::SyncManifest {
self
}
fn write_key(&mut self, _key: EncryptedKey) -> Result<()> {
Err(BackendError::Unknown(
"Changing the key of a FlatFile repository is not supported at this time.".to_string(),
))
}
fn read_key(&mut self) -> Result<EncryptedKey> {
Ok(self.enc_key.clone())
}
fn read_chunk(&mut self, location: SegmentDescriptor) -> Result<Chunk> {
let start = location.start;
let length = *self.length_map.get(&location).ok_or_else(|| {
BackendError::SegmentError(format!(
"Attempted to look up chunk with location {:?}, but its length was not known",
location
))
})?;
let file = &mut self.file;
file.seek(SeekFrom::Start(start))?;
let buffer_len: usize = length
.try_into()
.expect("Attempted to read a chunk that could not possibly fit into memory");
let mut buffer = vec![0_u8; buffer_len];
file.read_exact(&mut buffer[..])?;
let header = self
.chunk_headers
.get(&location)
.ok_or_else(|| {
BackendError::SegmentError(format!(
"Attempted to look up chunk with location {:?},\
but there was no associated chunk header",
location
))
})?
.clone();
let chunk = Chunk::unsplit(header, ChunkBody(buffer));
Ok(chunk)
}
fn write_chunk(&mut self, chunk: Chunk) -> Result<SegmentDescriptor> {
let id = chunk.get_id();
let file = &mut self.file;
let location = file.seek(SeekFrom::End(0))?;
let (header, body) = chunk.split();
let length = body.0.len() as u64;
let descriptor = SegmentDescriptor {
segment_id: 0,
start: location,
};
self.length_map.insert(descriptor, length);
self.entry_footer_data.add_chunk(id, location, length);
self.entry_footer_data.add_header(id, header.clone());
self.chunk_headers.insert(descriptor, header);
file.write_all(&body.0[..])?;
Ok(descriptor)
}
}
impl<T: Read + Write + Seek + 'static> Drop for GenericFlatFile<T> {
fn drop(&mut self) {
let res = self.commit_index();
if res.is_err() && !std::thread::panicking() {
panic!(
"Failed to commit index during drop. Path was {:?}",
self.path
)
}
}
}