use std::{fmt, fs, io};
use std::borrow::Cow;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use bytes::Bytes;
use log::trace;
use serde::{Deserialize, Serialize};
use serde::de::DeserializeOwned;
use rpki::uri;
use rpki::ca::publication::{Base64, ListElement, Publish, Update, Withdraw};
use rpki::rrdp::Hash;
use crate::commons::error::KrillIoError;
pub fn sub_dir(base: &Path, name: &str) -> Result<PathBuf, KrillIoError> {
let mut full_path = base.to_path_buf();
full_path.push(name);
create_dir_all(&full_path)?;
Ok(full_path)
}
pub fn create_dir_all(dir: &Path) -> Result<(), KrillIoError> {
if !dir.is_dir() {
fs::create_dir_all(dir).map_err(|e| {
KrillIoError::new(
format!("could not create dir: {}", dir.to_string_lossy()),
e,
)
})?;
}
Ok(())
}
pub fn remove_dir_all(dir: &Path) -> Result<(), KrillIoError> {
if dir.exists() {
fs::remove_dir_all(dir).map_err(|e| {
KrillIoError::new(
format!("could not remove dir: {}", dir.to_string_lossy()),
e,
)
})?;
}
Ok(())
}
pub fn create_file_with_path(path: &Path) -> Result<File, KrillIoError> {
if !path.exists() && let Some(parent) = path.parent() {
trace!("Creating path: {}", parent.to_string_lossy());
fs::create_dir_all(parent).map_err(|e| {
KrillIoError::new(
format!(
"Could not create dir path for: {}",
parent.to_string_lossy()
),
e,
)
})?;
}
File::create(path).map_err(|e| {
KrillIoError::new(
format!("Could not create file: {}", path.to_string_lossy()),
e,
)
})
}
pub fn file_path(base_path: &Path, file_name: &str) -> PathBuf {
let mut path = base_path.to_path_buf();
path.push(file_name);
path
}
pub fn save(content: &[u8], full_path: &Path) -> Result<(), KrillIoError> {
let mut f = create_file_with_path(full_path)?;
f.write_all(content).map_err(|e| {
KrillIoError::new(
format!("Could not write to: {}", full_path.to_string_lossy()),
e,
)
})?;
trace!("Saved file: {}", full_path.to_string_lossy());
Ok(())
}
pub fn save_json<O: Serialize>(
object: &O,
full_path: &Path,
) -> Result<(), KrillIoError> {
let json = serde_json::to_string(object).unwrap();
save(&Bytes::from(json), full_path)
}
pub fn load_json<O: DeserializeOwned>(
full_path: &Path,
) -> Result<O, KrillIoError> {
let bytes = read(full_path)?;
serde_json::from_slice(&bytes).map_err(|e| {
KrillIoError::new(
format!(
"Could not load json for file: {}",
full_path.to_string_lossy()
),
io::Error::other(
format!("could not deserialize json: {e}"),
),
)
})
}
pub fn save_in_dir(
content: &Bytes,
base_path: &Path,
name: &str,
) -> Result<(), KrillIoError> {
let mut full_path = base_path.to_path_buf();
full_path.push(name);
save(content, &full_path)
}
pub fn save_with_rsync_uri(
content: &Bytes,
base_path: &Path,
uri: &uri::Rsync,
) -> Result<(), KrillIoError> {
let path = path_with_rsync(base_path, uri);
save(content, &path)
}
pub fn read(path: &Path) -> Result<Bytes, KrillIoError> {
let mut f = File::open(path).map_err(|e| {
KrillIoError::new(
format!("Could not open: '{}'", path.to_string_lossy()),
e,
)
})?;
let mut bytes = Vec::new();
f.read_to_end(&mut bytes).map_err(|e| {
KrillIoError::new(
format!("Could not read: {}", path.to_string_lossy()),
e,
)
})?;
Ok(Bytes::from(bytes))
}
pub fn read_with_rsync_uri(
base_path: &Path,
uri: &uri::Rsync,
) -> Result<Bytes, KrillIoError> {
let path = path_with_rsync(base_path, uri);
read(&path)
}
pub fn delete_with_rsync_uri(
base_path: &Path,
uri: &uri::Rsync,
) -> Result<(), KrillIoError> {
delete_file(&path_with_rsync(base_path, uri))
}
pub fn delete_in_dir(
base_path: &Path,
name: &str,
) -> Result<(), KrillIoError> {
let mut full_path = base_path.to_path_buf();
full_path.push(name);
delete_file(&full_path)
}
pub fn delete_file(full_path: &Path) -> Result<(), KrillIoError> {
trace!("Removing file: {}", full_path.to_string_lossy());
fs::remove_file(full_path).map_err(|e| {
KrillIoError::new(
format!("Could not remove file: {}", full_path.to_string_lossy()),
e,
)
})
}
pub fn clean_file_and_path(path: &Path) -> Result<(), KrillIoError> {
if path.exists() {
delete_file(path)?;
let mut parent_opt = path.parent();
while parent_opt.is_some() {
let parent = parent_opt.unwrap();
if parent
.read_dir()
.map_err(|e| {
KrillIoError::new(
format!(
"Could not read directory: '{}'",
parent.to_string_lossy()
),
e,
)
})?
.count()
== 0
{
trace!(
"Will delete {}", parent.display(),
);
fs::remove_dir(parent).map_err(|e| {
KrillIoError::new(
format!(
"Could not remove dir: {}",
parent.display(),
),
e,
)
})?;
}
parent_opt = parent.parent();
}
}
Ok(())
}
fn path_with_rsync(base_path: &Path, uri: &uri::Rsync) -> PathBuf {
let mut path = base_path.to_path_buf();
path.push(uri.authority());
path.push(uri.module_name());
path.push(uri.path());
path
}
pub fn crawl_incl_rsync_base(
base_path: &Path,
rsync_base: &uri::Rsync,
) -> Result<Vec<CurrentFile>, Error> {
crawl_disk(base_path, base_path, Some(rsync_base))
}
pub fn crawl_derive_rsync_uri(
base_path: &Path,
) -> Result<Vec<CurrentFile>, Error> {
crawl_disk(base_path, base_path, None)
}
fn crawl_disk(
base_path: &Path,
path: &Path,
rsync_base: Option<&uri::Rsync>,
) -> Result<Vec<CurrentFile>, Error> {
let mut res = Vec::new();
for entry in fs::read_dir(path).map_err(|_| Error::cannot_read(path))? {
let entry = entry.map_err(|_| Error::cannot_read(path))?;
let path = entry.path();
if path.is_dir() {
let mut other = crawl_disk(base_path, &path, rsync_base)?;
res.append(&mut other);
} else {
let uri = derive_uri(base_path, &path, rsync_base)?;
let content =
read(&path).map_err(|_| Error::cannot_read(&path))?;
let current_file = CurrentFile::new(uri, &content);
res.push(current_file);
}
}
Ok(res)
}
fn derive_uri(
base_path: &Path,
path: &Path,
rsync_base: Option<&uri::Rsync>,
) -> Result<uri::Rsync, Error> {
let rel = path
.strip_prefix(base_path)
.map_err(|_| Error::PathOutsideBasePath)?;
let rel_string = rel.to_string_lossy().to_string();
let uri_string = match rsync_base {
Some(rsync_base) => format!("{rsync_base}{rel_string}"),
None => format!("rsync://{rel_string}"),
};
let uri = uri::Rsync::from_str(&uri_string)
.map_err(|_| Error::UnsupportedFileName(uri_string))?;
Ok(uri)
}
pub fn backup_dir(base_path: &Path, target_path: &Path) -> Result<(), Error> {
if base_path.to_string_lossy() == Cow::Borrowed("/")
|| target_path.to_string_lossy() == Cow::Borrowed("/")
{
Err(Error::BackupExcessive)
} else if base_path.is_file() {
let mut target = target_path.to_path_buf();
target.push(base_path.file_name().unwrap());
if target.exists() {
Err(Error::backup_target_exists(target_path))
} else {
fs::copy(base_path, target_path).map_err(|e| {
KrillIoError::new(
format!(
"Could not back up file from '{}' to '{}'",
base_path.to_string_lossy(),
target_path.to_string_lossy()
),
e,
)
})?;
Ok(())
}
} else if base_path.is_dir() {
for entry in fs::read_dir(base_path).map_err(|e| {
KrillIoError::new(
format!(
"Could not read dir '{}' for backup",
base_path.to_string_lossy()
),
e,
)
})? {
let path = entry
.map_err(|e| {
KrillIoError::new(
format!(
"Could not read entry for dir '{}' for backup",
base_path.to_string_lossy()
),
e,
)
})?
.path();
let mut target = target_path.to_path_buf();
target.push(path.file_name().unwrap());
if path.is_dir() {
backup_dir(&path, &target)?;
} else if path.is_file() {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).map_err(|e| {
KrillIoError::new(
format!(
"Could not create dir(s) '{}' for backup",
parent.to_string_lossy()
),
e,
)
})?;
}
fs::copy(&path, &target).map_err(|e| {
KrillIoError::new(
format!(
"Could not backup '{}' to '{}'",
path.to_string_lossy(),
target.to_string_lossy()
),
e,
)
})?;
} else {
return Err(Error::backup_cannot_read(&path));
}
}
Ok(())
} else {
Err(Error::backup_cannot_read(base_path))
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CurrentFile {
uri: uri::Rsync,
content: Base64,
hash: Hash,
}
impl CurrentFile {
pub fn new(uri: uri::Rsync, content: &Bytes) -> Self {
let content = Base64::from_content(content);
let hash = content.to_hash();
CurrentFile { uri, content, hash }
}
pub fn save(&self, base_path: &Path) -> Result<(), KrillIoError> {
save_with_rsync_uri(&self.content.to_bytes(), base_path, &self.uri)
}
pub fn uri(&self) -> &uri::Rsync {
&self.uri
}
pub fn content(&self) -> &Base64 {
&self.content
}
pub fn to_bytes(&self) -> Bytes {
self.content.to_bytes()
}
pub fn hash(&self) -> &Hash {
&self.hash
}
pub fn as_publish(&self) -> Publish {
let tag = Some(self.hash.to_string());
let uri = self.uri.clone();
let content = self.content.clone();
Publish::new(tag, uri, content)
}
pub fn as_update(&self, old_hash: &Hash) -> Update {
let tag = None;
let uri = self.uri.clone();
let content = self.content.clone();
let hash = *old_hash;
Update::new(tag, uri, content, hash)
}
pub fn as_withdraw(&self) -> Withdraw {
let tag = None;
let uri = self.uri.clone();
let hash = self.hash;
Withdraw::new(tag, uri, hash)
}
pub fn into_list_element(self) -> ListElement {
ListElement::new(self.uri, self.hash)
}
}
impl PartialEq for CurrentFile {
fn eq(&self, other: &CurrentFile) -> bool {
self.uri == other.uri
&& self.hash == other.hash
&& self.content == other.content
}
}
impl Eq for CurrentFile {}
#[derive(Debug)]
pub enum Error {
CannotRead(String),
UnsupportedFileName(String),
PathOutsideBasePath,
BackupExcessive,
BackupCannotReadSource(String),
BackupTargetExists(String),
Io(KrillIoError),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::CannotRead(s) => write!(f, "Cannot read: {s}"),
Error::UnsupportedFileName(name) => {
write!(f, "Unsupported characters: {name}")
}
Error::PathOutsideBasePath => {
write!(f, "Cannot use path outside of rsync jail")
}
Error::BackupExcessive => write!(
f,
"Do not ever use '/' as the source or target for backups"
),
Error::BackupCannotReadSource(e) => {
write!(f, "Source for backup cannot be read: {e}")
}
Error::BackupTargetExists(e) => {
write!(f, "Target for backup already exists: {e}")
}
Error::Io(e) => e.fmt(f),
}
}
}
impl Error {
pub fn cannot_read(path: &Path) -> Error {
let str = path.to_string_lossy().to_string();
Error::CannotRead(str)
}
fn backup_cannot_read(path: &Path) -> Error {
let str = path.to_string_lossy().to_string();
Error::BackupCannotReadSource(str)
}
fn backup_target_exists(path: &Path) -> Error {
let str = path.to_string_lossy().to_string();
Error::BackupTargetExists(str)
}
}
impl std::error::Error for Error {}
impl From<KrillIoError> for Error {
fn from(e: KrillIoError) -> Self {
Error::Io(e)
}
}
#[cfg(test)]
mod tests {
use crate::commons::test;
use super::*;
#[test]
fn should_scan_disk() {
test::test_under_tmp(|base_dir| {
let file_1 = CurrentFile::new(
test::rsync("rsync://host:10873/module/alice/file1.txt"),
&Bytes::from("content 1"),
);
let file_2 = CurrentFile::new(
test::rsync("rsync://host:10873/module/alice/file2.txt"),
&Bytes::from("content 2"),
);
let file_3 = CurrentFile::new(
test::rsync("rsync://host:10873/module/alice/sub/file1.txt"),
&Bytes::from("content sub file"),
);
let file_4 = CurrentFile::new(
test::rsync("rsync://host:10873/module/bob/file.txt"),
&Bytes::from("content"),
);
file_1.save(&base_dir).unwrap();
file_2.save(&base_dir).unwrap();
file_3.save(&base_dir).unwrap();
file_4.save(&base_dir).unwrap();
let files = crawl_derive_rsync_uri(&base_dir).unwrap();
assert!(files.contains(&file_1));
assert!(files.contains(&file_2));
assert!(files.contains(&file_3));
assert!(files.contains(&file_4));
});
}
}