use crate::network::NetworkConfig;
use crate::util::{disk_size::DiskSize, fs::move_dir};
use crate::{mar::config::MarConfig, util::fs::move_file};
use crate::{
mar::{CollectionTime, Manifest, MarEntry, Metadata},
util::fs::copy_file,
};
use eyre::{eyre, Result, WrapErr};
use std::ffi::OsStr;
use std::fs::{create_dir, remove_dir_all, rename, File, Metadata as FsMetadata};
use std::io;
use std::mem::take;
use std::path::{Path, PathBuf};
use uuid::Uuid;
pub const MAR_ENTRY_OVERHEAD_SIZE_ESTIMATE: u64 = 4096;
pub struct MarEntryBuilder<M> {
entry_dir: MarEntryDir,
uuid: Uuid,
collection_time: CollectionTime,
metadata: M,
attachments: Vec<MarAttachment>,
}
pub struct NoMetadata;
impl<M> MarEntryBuilder<M> {
fn entry_dir_path(&self) -> &Path {
&self.entry_dir.path
}
pub fn make_attachment_path_in_entry_dir<F: AsRef<str>>(&self, filename: F) -> PathBuf {
self.entry_dir_path().join(filename.as_ref())
}
pub fn add_attachment(mut self, file: PathBuf) -> Result<MarEntryBuilder<M>> {
if file.is_file() && file.is_absolute() {
self.attachments.push(MarAttachment::Move(file));
Ok(self)
} else {
Err(eyre!("Failed to add attachment!"))
}
}
pub fn add_copied_attachment(mut self, file: PathBuf) -> Result<MarEntryBuilder<M>> {
if file.is_file() && file.is_absolute() {
self.attachments.push(MarAttachment::Copy(file));
Ok(self)
} else {
Err(eyre!("Failed to add copied attachment!"))
}
}
}
impl MarEntryBuilder<NoMetadata> {
pub fn new(mar_staging: &Path) -> eyre::Result<MarEntryBuilder<NoMetadata>> {
let collection_time = CollectionTime::now()?;
let uuid = Uuid::new_v4();
let path = mar_staging.to_owned().join(uuid.to_string());
create_dir(&path)?;
Ok(Self {
entry_dir: MarEntryDir::new(path),
uuid,
collection_time,
metadata: NoMetadata,
attachments: vec![],
})
}
pub fn set_metadata(self, metadata: Metadata) -> MarEntryBuilder<Metadata> {
MarEntryBuilder {
entry_dir: self.entry_dir,
uuid: self.uuid,
collection_time: self.collection_time,
attachments: self.attachments,
metadata,
}
}
}
impl MarEntryBuilder<Metadata> {
pub fn save(self, network_config: &NetworkConfig, mar_config: &MarConfig) -> Result<MarEntry> {
for filepath in self.attachments {
if let Some(filename) = filepath.file_name() {
let target = self.entry_dir.path.join(filename);
match filepath {
MarAttachment::Copy(source) => {
copy_file(&source, &target)?;
}
MarAttachment::Move(source) => {
move_file(&source, &target)?;
}
}
}
}
let manifest = Manifest::new(network_config, self.collection_time, self.metadata);
let manifest_path = self.entry_dir.path.join("manifest.tmp");
{
let manifest_file = File::create(&manifest_path)
.wrap_err_with(|| format!("Error opening manifest {}", manifest_path.display()))?;
serde_json::to_writer(manifest_file, &manifest)?;
}
let manifest_json_path = manifest_path.with_extension("json");
rename(&manifest_path, &manifest_json_path).wrap_err_with(|| {
format!(
"Error renaming manifest {} to {}",
manifest_path.display(),
manifest_json_path.display()
)
})?;
let final_staging_dir = mar_config.final_staging_path(&manifest.metadata);
let tmp_staging_dir = mar_config.tmp_staging_path();
let final_entry_path = final_staging_dir.join(self.uuid.to_string());
if final_staging_dir != tmp_staging_dir {
move_dir(&self.entry_dir.path, &final_entry_path)?;
}
self.entry_dir.mark_saved();
Ok(MarEntry {
path: final_entry_path,
uuid: self.uuid,
manifest,
})
}
pub fn estimated_entry_size(&self) -> DiskSize {
let attachments_size_bytes: u64 = self
.attachments
.iter()
.filter_map(|p| p.metadata().ok())
.map(|m| m.len())
.sum();
DiskSize {
bytes: attachments_size_bytes + MAR_ENTRY_OVERHEAD_SIZE_ESTIMATE,
inodes: self.attachments.len() as u64 + 1,
}
}
pub fn get_metadata(&self) -> &Metadata {
&self.metadata
}
}
#[derive(Debug)]
enum MarAttachment {
Copy(PathBuf),
Move(PathBuf),
}
impl MarAttachment {
fn file_name(&self) -> Option<&OsStr> {
match self {
MarAttachment::Copy(path) | MarAttachment::Move(path) => path.file_name(),
}
}
fn metadata(&self) -> io::Result<FsMetadata> {
match self {
MarAttachment::Copy(path) | MarAttachment::Move(path) => path.metadata(),
}
}
}
struct MarEntryDir {
path: PathBuf,
saved: bool,
}
impl MarEntryDir {
fn new(path: PathBuf) -> Self {
Self { path, saved: false }
}
fn mark_saved(mut self) -> PathBuf {
self.saved = true;
take(&mut self.path)
}
}
impl Drop for MarEntryDir {
fn drop(&mut self) {
if !self.saved {
let _ = remove_dir_all(&self.path);
}
}
}
#[cfg(test)]
mod tests {
use super::MAR_ENTRY_OVERHEAD_SIZE_ESTIMATE;
use crate::network::NetworkConfig;
use crate::test_utils::create_file_with_size;
use crate::{config::Config, mar::MarEntryBuilder};
use crate::{
config::PersistStorageConfig,
mar::{MarConfig, Metadata},
};
use rstest::{fixture, rstest};
use std::{fs::create_dir_all, path::PathBuf};
use tempfile::{tempdir, TempDir};
#[rstest]
fn cleans_up_entry_dir_when_save_was_not_called(fixture: Fixture) {
let builder = MarEntryBuilder::new(&fixture.mar_staging).unwrap();
let entry_dir = builder.entry_dir_path().to_owned();
assert!(entry_dir.exists());
create_file_with_size(&entry_dir.join("attachment"), 1024).unwrap();
drop(builder);
assert!(!entry_dir.exists());
}
#[rstest]
fn save_keeps_entry_dir_and_adds_manifest_json(fixture: Fixture) {
let mut entry_dir_option = None;
{
let builder = MarEntryBuilder::new(&fixture.mar_staging).unwrap();
let _ = entry_dir_option.insert(builder.entry_dir_path().to_owned());
builder
.set_metadata(Metadata::test_fixture())
.save(
&NetworkConfig::test_fixture(),
&MarConfig::from(&Config::test_fixture()),
)
.unwrap();
}
let entry_dir = entry_dir_option.unwrap();
assert!(entry_dir.exists());
assert!(entry_dir.join("manifest.json").exists());
}
#[rstest]
fn create_attachment_inside_entry_dir(fixture: Fixture) {
let builder = MarEntryBuilder::new(&fixture.mar_staging).unwrap();
let orig_attachment_path = builder.make_attachment_path_in_entry_dir("attachment");
create_file_with_size(&orig_attachment_path, 1024).unwrap();
builder
.add_attachment(orig_attachment_path.clone())
.unwrap()
.set_metadata(Metadata::test_fixture())
.save(
&NetworkConfig::test_fixture(),
&MarConfig::from(&Config::test_fixture()),
)
.unwrap();
assert!(orig_attachment_path.exists());
}
#[rstest]
fn attachment_outside_entry_dir_is_moved_into_entry_dir_upon_save(fixture: Fixture) {
let builder = MarEntryBuilder::new(&fixture.mar_staging).unwrap();
let entry_dir = builder.entry_dir_path().to_owned();
let tempdir = tempdir().unwrap();
let orig_attachment_path = tempdir.path().join("attachment");
create_file_with_size(&orig_attachment_path, 1024).unwrap();
builder
.add_attachment(orig_attachment_path.clone())
.unwrap()
.set_metadata(Metadata::test_fixture())
.save(
&NetworkConfig::test_fixture(),
&MarConfig::from(&Config::test_fixture()),
)
.unwrap();
assert!(!orig_attachment_path.exists());
assert!(entry_dir
.join(orig_attachment_path.file_name().unwrap())
.exists());
}
#[rstest]
fn can_estimate_size_of_a_mar_entry(fixture: Fixture) {
let builder = MarEntryBuilder::new(&fixture.mar_staging).unwrap();
let orig_attachment_path = builder.make_attachment_path_in_entry_dir("attachment");
create_file_with_size(&orig_attachment_path, 1024).unwrap();
let builder = builder
.add_attachment(orig_attachment_path)
.unwrap()
.set_metadata(Metadata::test_fixture());
assert_eq!(
builder.estimated_entry_size().bytes,
1024 + MAR_ENTRY_OVERHEAD_SIZE_ESTIMATE
);
assert_eq!(builder.estimated_entry_size().inodes, 2);
}
#[rstest]
fn entry_moved_to_persist_dir_on_save(fixture: Fixture) {
let tmp_staging_dir = fixture.mar_staging.join("tmp");
let persist_staging_dir = fixture.mar_staging.join("persist");
create_dir_all(&tmp_staging_dir).unwrap();
create_dir_all(&persist_staging_dir).unwrap();
let persist_config = PersistStorageConfig {
logs: true,
metrics: true,
coredumps: true,
reboots: true,
min_headroom: 1024,
max_usage: 1024 * 1024 * 1024,
min_inodes: 1024,
};
let mar_config = MarConfig::test_fixture_with_config(
&tmp_staging_dir,
&persist_staging_dir,
persist_config,
);
let builder = MarEntryBuilder::new(&tmp_staging_dir).unwrap();
let entry_dir = builder.entry_dir_path().to_owned();
let tempdir = tempdir().unwrap();
let orig_attachment_path = tempdir.path().join("attachment");
create_file_with_size(&orig_attachment_path, 1024).unwrap();
let persist_entry_dir = persist_staging_dir.join(builder.uuid.to_string());
let final_path = builder
.set_metadata(Metadata::test_fixture_metrics())
.save(&NetworkConfig::test_fixture(), &mar_config)
.unwrap();
assert!(!entry_dir.exists());
assert!(final_path.path.starts_with(persist_entry_dir));
assert!(final_path.path.exists());
}
struct Fixture {
_tempdir: TempDir,
mar_staging: PathBuf,
}
#[fixture]
fn fixture() -> Fixture {
let tempdir = tempdir().unwrap();
let mar_staging = tempdir.path().to_owned();
Fixture {
_tempdir: tempdir,
mar_staging,
}
}
}