use std::{
collections::{HashMap, HashSet},
env::{current_dir, current_exe},
ffi::OsStr,
fs::{self, File, create_dir, remove_dir, remove_dir_all, remove_file},
hash::Hash,
io::{self, Write},
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
use thiserror::Error;
const ZERO: u64 = 0;
const THOUSAND: u64 = 1_000;
const MILLION: u64 = 1_000_000;
const BILLION: u64 = 1_000_000_000;
const TRILLION: u64 = 1_000_000_000_000;
const QUADRILLION: u64 = 1_000_000_000_000_000;
#[derive(Debug, Error)]
pub enum DatabaseError {
#[error("Steps '{0}' greater than path length '{1}'")]
PathStepOverflow(i32, i32),
#[error("Directory '{0}' not found along path to executable")]
NoClosestDir(String),
#[error("ID '{0}' doesn't point to a known path")]
NoMatchingID(String),
#[error("ID '{0}' already exists")]
IdAlreadyExists(String),
#[error("Source and destination are identical: '{0}'")]
IdenticalSourceDestination(PathBuf),
#[error("Export destination is inside the database: '{0}'")]
ExportDestinationInsideDatabase(PathBuf),
#[error("Import source is inside the database: '{0}'")]
ImportSourceInsideDatabase(PathBuf),
#[error("Root database ID cannot be used for this operation")]
RootIdUnsupported,
#[error("Path '{0}' doesn't point to a directory")]
NotADirectory(PathBuf),
#[error("Path '{0}' doesn't point to a file")]
NotAFile(PathBuf),
#[error("Couldn't convert OsString to String")]
OsStringConversion,
#[error("ID '{0}' doesn't have a parent")]
NoParent(String),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
#[error(transparent)]
Bincode(#[from] bincode::Error),
#[error(transparent)]
PathBufConversion(#[from] std::path::StripPrefixError),
}
#[derive(Debug, PartialEq, Clone, Default)]
pub enum ForceDeletion {
#[default]
Force,
NoForce,
}
impl From<ForceDeletion> for bool {
fn from(val: ForceDeletion) -> Self {
match val {
ForceDeletion::Force => true,
ForceDeletion::NoForce => false,
}
}
}
impl From<bool> for ForceDeletion {
fn from(value: bool) -> Self {
match value {
true => ForceDeletion::Force,
false => ForceDeletion::NoForce,
}
}
}
#[derive(Debug, PartialEq, Clone, Default)]
pub enum ShouldSort {
#[default]
Sort,
NoSort,
}
impl From<ShouldSort> for bool {
fn from(val: ShouldSort) -> Self {
match val {
ShouldSort::Sort => true,
ShouldSort::NoSort => false,
}
}
}
impl From<bool> for ShouldSort {
fn from(value: bool) -> Self {
match value {
true => ShouldSort::Sort,
false => ShouldSort::NoSort,
}
}
}
#[derive(Debug, PartialEq, Clone, Default)]
pub enum ExportMode {
#[default]
Copy,
Move,
}
#[derive(Debug, PartialEq, Clone, Default)]
pub enum ScanPolicy {
DetectOnly,
RemoveNew,
#[default]
AddNew,
}
#[derive(Debug, Default, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)]
pub enum FileSizeUnit {
#[default]
Byte,
Kilobyte,
Megabyte,
Gigabyte,
Terabyte,
Petabyte,
}
impl FileSizeUnit {
fn variant_integer_id(&self) -> u8 {
match self {
Self::Byte => 0,
Self::Kilobyte => 1,
Self::Megabyte => 2,
Self::Gigabyte => 3,
Self::Terabyte => 4,
Self::Petabyte => 5,
}
}
}
#[derive(PartialEq, Debug, Clone, Default)]
pub struct GenPath;
impl GenPath {
pub fn from_working_dir(steps: i32) -> Result<PathBuf, DatabaseError> {
let working_dir = truncate(current_dir()?, steps)?;
Ok(working_dir)
}
pub fn from_exe(steps: i32) -> Result<PathBuf, DatabaseError> {
let exe = truncate(current_exe()?, steps + 1)?;
Ok(exe)
}
pub fn from_closest_match(name: impl AsRef<Path>) -> Result<PathBuf, DatabaseError> {
let exe = current_exe()?;
let target = name.as_ref();
let target_name = match target.file_name() {
Some(name) => name,
None => return Err(DatabaseError::OsStringConversion)
};
for path in exe.ancestors() {
if !path.is_dir() {
continue;
}
if path
.file_name()
.is_some_and(|dir_name| dir_name == target_name)
{
return Ok(path.to_path_buf());
}
if let Ok(entries) = fs::read_dir(path) {
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(_) => continue,
};
let child_path = entry.path();
if !child_path.is_dir() {
continue;
}
if entry.file_name() == target_name {
return Ok(child_path);
}
}
}
}
let name_as_string = match target.to_owned().into_os_string().into_string() {
Ok(string) => string,
Err(_) => return Err(DatabaseError::OsStringConversion),
};
Err(DatabaseError::NoClosestDir(name_as_string))
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
pub struct ItemId {
name: String,
index: usize,
}
impl<T> From<T> for ItemId
where
T: Into<String>,
{
fn from(value: T) -> Self {
Self {
name: value.into(),
index: 0,
}
}
}
impl From<&ItemId> for ItemId {
fn from(value: &ItemId) -> Self {
value.clone()
}
}
impl ItemId {
pub fn database_id() -> Self {
Self {
name: String::new(),
index: 0,
}
}
pub fn id(id: impl Into<String>) -> Self {
Self {
name: id.into(),
index: 0,
}
}
pub fn with_index(id: impl Into<String>, index: usize) -> Self {
Self::from(id, index)
}
pub fn from(id: impl Into<String>, index: usize) -> Self {
Self {
name: id.into(),
index,
}
}
pub fn get_name(&self) -> &str {
&self.name
}
pub fn get_index(&self) -> usize {
self.index
}
pub fn as_str(&self) -> &str {
self.get_name()
}
pub fn as_string(&self) -> String {
self.name.clone()
}
}
#[derive(Debug, Default, PartialEq, PartialOrd, Clone, Copy)]
pub struct FileSize {
size: u64,
unit: FileSizeUnit,
}
impl FileSize {
pub fn get_size(&self) -> u64 {
self.size
}
pub fn get_unit(&self) -> FileSizeUnit {
self.unit
}
pub fn unit_as_string(&self) -> String {
let name = match self.unit {
FileSizeUnit::Byte => "Byte",
FileSizeUnit::Kilobyte => "Kilobyte",
FileSizeUnit::Megabyte => "Megabyte",
FileSizeUnit::Gigabyte => "Gigabyte",
FileSizeUnit::Terabyte => "Terabyte",
FileSizeUnit::Petabyte => "Petabyte",
};
let mut name_string = String::from(name);
match self.size {
1 => (),
_ => name_string.push('s'),
}
name_string
}
pub fn as_unit(&self, unit: FileSizeUnit) -> Self {
let difference = self.unit.variant_integer_id() as i8 - unit.variant_integer_id() as i8;
let mut size = self.size;
if difference > 0 {
let factor = THOUSAND.pow(difference as u32);
size = size.saturating_mul(factor);
} else if difference < 0 {
let factor = THOUSAND.pow((-difference) as u32);
size /= factor;
}
Self { size, unit }
}
fn from(bytes: u64) -> Self {
let (size, unit) = match bytes {
ZERO..THOUSAND => (bytes, FileSizeUnit::Byte),
THOUSAND..MILLION => (bytes / THOUSAND, FileSizeUnit::Kilobyte),
MILLION..BILLION => (bytes / MILLION, FileSizeUnit::Megabyte),
BILLION..TRILLION => (bytes / BILLION, FileSizeUnit::Gigabyte),
TRILLION..QUADRILLION => (bytes / TRILLION, FileSizeUnit::Terabyte),
_ => (bytes / QUADRILLION, FileSizeUnit::Petabyte),
};
Self { size, unit }
}
}
#[derive(Debug, Default, PartialEq, PartialOrd, Clone)]
pub struct FileInformation {
name: Option<String>,
extension: Option<String>,
size: FileSize,
unix_created: Option<u64>,
time_since_created: Option<u64>,
unix_last_opened: Option<u64>,
time_since_last_opened: Option<u64>,
unix_last_modified: Option<u64>,
time_since_last_modified: Option<u64>,
}
impl FileInformation {
pub fn get_name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn get_extension(&self) -> Option<&str> {
self.extension.as_deref()
}
pub fn get_size(&self) -> &FileSize {
&self.size
}
pub fn get_unix_created(&self) -> Option<&u64> {
self.unix_created.as_ref()
}
pub fn get_time_since_created(&self) -> Option<&u64> {
self.time_since_created.as_ref()
}
pub fn get_unix_last_opened(&self) -> Option<&u64> {
self.unix_last_opened.as_ref()
}
pub fn get_time_since_last_opened(&self) -> Option<&u64> {
self.time_since_last_opened.as_ref()
}
pub fn get_unix_last_modified(&self) -> Option<&u64> {
self.unix_last_modified.as_ref()
}
pub fn get_time_since_last_modified(&self) -> Option<&u64> {
self.time_since_last_modified.as_ref()
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum ExternalChange {
Added { id: ItemId, path: PathBuf },
Removed { id: ItemId, path: PathBuf },
}
#[derive(Debug, PartialEq, Clone)]
pub struct ScanReport {
scanned_from: ItemId,
recursive: bool,
added: Vec<ExternalChange>,
removed: Vec<ExternalChange>,
unchanged_count: usize,
total_changed_count: usize,
}
impl ScanReport {
pub fn get_scan_from(&self) -> &ItemId {
&self.scanned_from
}
pub fn get_added(&self) -> &Vec<ExternalChange> {
&self.added
}
pub fn get_removed(&self) -> &Vec<ExternalChange> {
&self.removed
}
pub fn get_unchanged_count(&self) -> usize {
self.unchanged_count
}
pub fn get_total_changed_count(&self) -> usize {
self.total_changed_count
}
}
#[derive(Debug, PartialEq, Default)]
struct StableVec<T> {
list: Vec<Option<T>>,
free: Vec<usize>,
}
impl<T> StableVec<T> {
fn push(&mut self, data: T) -> usize {
if let Some(index) = self.free.pop() {
self.list[index] = Some(data);
index
} else {
self.list.push(Some(data));
self.list.len() - 1
}
}
fn remove(&mut self, index: usize) -> bool {
if index >= self.list.len() || self.list[index].is_none() {
return false;
}
self.list[index] = None;
self.free.push(index);
true
}
fn get(&self, index: usize) -> Option<&T> {
self.list.get(index).and_then(|value| value.as_ref())
}
fn insert_at(&mut self, index: usize, data: T) -> bool {
if index < self.list.len() {
if self.list[index].is_some() {
return false;
}
self.list[index] = Some(data);
if let Some(free_index) = self.free.iter().position(|slot| *slot == index) {
self.free.swap_remove(free_index);
}
return true;
}
while self.list.len() < index {
let free_slot = self.list.len();
self.list.push(None);
self.free.push(free_slot);
}
self.list.push(Some(data));
true
}
fn iter(&self) -> impl Iterator<Item = (usize, &T)> {
self.list
.iter()
.enumerate()
.filter_map(|(index, value)| value.as_ref().map(|value| (index, value)))
}
fn is_empty(&self) -> bool {
self.list.iter().all(Option::is_none)
}
}
#[derive(Debug, PartialEq)]
pub struct DatabaseManager {
path: PathBuf,
items: HashMap<String, StableVec<PathBuf>>,
}
impl DatabaseManager {
pub fn create_database(path: impl AsRef<Path>, name: impl AsRef<Path>) -> Result<Self, DatabaseError> {
let mut path: PathBuf = path.as_ref().to_path_buf();
path.push(name);
if !path.exists() {
create_dir(&path)?;
} else if !path.is_dir() {
return Err(DatabaseError::NotADirectory(path));
}
let mut manager = Self {
path,
items: HashMap::new(),
};
let discovered = manager.collect_paths_in_scope(&manager.path, true)?;
for relative_path in discovered {
let name = os_str_to_string(relative_path.file_name())?;
manager.insert_generated_path(name, relative_path);
}
Ok(manager)
}
pub fn write_new(
&mut self,
id: impl Into<ItemId>,
parent: impl Into<ItemId>,
) -> Result<(), DatabaseError> {
let id = id.into();
let parent = parent.into();
if id.get_name().is_empty() {
return Err(DatabaseError::RootIdUnsupported);
}
let absolute_parent_path = self.locate_absolute(&parent)?;
let relative_path = if parent.get_name().is_empty() {
PathBuf::from(id.get_name())
} else {
let mut path = self.locate_relative(parent)?.to_path_buf();
path.push(id.get_name());
path
};
let absolute_path = absolute_parent_path.join(id.get_name());
if self.path_exists_in_index(&relative_path) {
return Err(DatabaseError::IdAlreadyExists(id.as_string()));
}
if relative_path.extension().is_none() {
create_dir(&absolute_path)?;
} else {
File::create_new(&absolute_path)?;
}
self.insert_path_for_id(&id, relative_path)?;
Ok(())
}
pub fn overwrite_existing<T>(&self, id: impl Into<ItemId>, data: T) -> Result<(), DatabaseError>
where
T: AsRef<[u8]>,
{
let id = id.into();
let bytes = data.as_ref();
let path = self.locate_absolute(id)?;
self.overwrite_path_atomic_with(&path, |file| {
file.write_all(bytes)?;
Ok(bytes.len() as u64)
})?;
Ok(())
}
pub fn overwrite_existing_json<T: serde::Serialize>(
&self,
id: impl Into<ItemId>,
value: &T,
pretty: impl Into<bool>,
) -> Result<(), DatabaseError> {
let pretty = pretty.into();
let data = if pretty {
serde_json::to_vec_pretty(value)?
} else {
serde_json::to_vec(value)?
};
self.overwrite_existing(id, data)
}
pub fn overwrite_existing_binary<T: serde::Serialize>(
&self,
id: impl Into<ItemId>,
value: &T,
) -> Result<(), DatabaseError> {
let data = bincode::serialize(value)?;
self.overwrite_existing(id, data)
}
pub fn overwrite_existing_from_reader<R: io::Read>(
&self,
id: impl Into<ItemId>,
reader: &mut R,
) -> Result<u64, DatabaseError> {
let id = id.into();
let path = self.locate_absolute(id)?;
self.overwrite_path_atomic_with(&path, |file| Ok(io::copy(reader, file)?))
}
pub fn read_existing(&self, id: impl Into<ItemId>) -> Result<Vec<u8>, DatabaseError> {
let id = id.into();
let path = self.locate_absolute(id)?;
if path.is_dir() {
return Err(DatabaseError::NotAFile(path));
}
Ok(fs::read(path)?)
}
pub fn read_existing_json<T: serde::de::DeserializeOwned>(
&self,
id: impl Into<ItemId>,
) -> Result<T, DatabaseError> {
let bytes = self.read_existing(id)?;
Ok(serde_json::from_slice(&bytes)?)
}
pub fn read_existing_binary<T: serde::de::DeserializeOwned>(
&self,
id: impl Into<ItemId>,
) -> Result<T, DatabaseError> {
let bytes = self.read_existing(id)?;
Ok(bincode::deserialize(&bytes)?)
}
pub fn get_all(&self, sorted: impl Into<bool>) -> Vec<ItemId> {
let sorted = sorted.into();
let mut list: Vec<ItemId> = self
.items
.iter()
.flat_map(|(name, paths)| {
paths
.iter()
.map(|(index, _)| ItemId::with_index(name.clone(), index))
.collect::<Vec<ItemId>>()
})
.collect();
if sorted {
list.sort();
}
list
}
pub fn get_by_parent(
&self,
parent: impl Into<ItemId>,
sorted: impl Into<bool>,
) -> Result<Vec<ItemId>, DatabaseError> {
let parent = parent.into();
let sorted = sorted.into();
let absolute_parent = self.locate_absolute(&parent)?;
if !absolute_parent.is_dir() {
return Err(DatabaseError::NotADirectory(absolute_parent));
}
let mut list: Vec<ItemId> = Vec::new();
let parent_path = if parent.get_name().is_empty() {
None
} else {
Some(self.locate_relative(&parent)?.as_path())
};
for (name, paths) in &self.items {
for (index, item_path) in paths.iter() {
let is_match = match parent_path {
None => item_path
.parent()
.is_some_and(|path| path.as_os_str().is_empty()),
Some(parent_path) => item_path.parent() == Some(parent_path),
};
if is_match {
list.push(ItemId::with_index(name.clone(), index));
}
}
}
if sorted {
list.sort();
}
Ok(list)
}
pub fn get_parent(&self, id: impl Into<ItemId>) -> Result<ItemId, DatabaseError> {
let id = id.into();
let path = self.locate_relative(&id)?;
let parent = match path.parent() {
Some(parent) => parent,
None => return Ok(ItemId::database_id()),
};
if parent.as_os_str().is_empty() {
return Ok(ItemId::database_id());
}
for (name, paths) in &self.items {
for (index, item_path) in paths.iter() {
if item_path.as_path() == parent {
return Ok(ItemId::with_index(name.clone(), index));
}
}
}
match parent.file_name() {
Some(name) => Ok(ItemId::id(os_str_to_string(Some(name))?)),
None => Err(DatabaseError::NoParent(id.as_string())),
}
}
pub fn rename(
&mut self,
id: impl Into<ItemId>,
to: impl AsRef<str>,
) -> Result<(), DatabaseError> {
let id = id.into();
let name = to.as_ref().to_owned();
if id.get_name().is_empty() {
return Err(DatabaseError::RootIdUnsupported);
}
let path = self.locate_absolute(&id)?;
let mut relative_path = self.locate_relative(&id)?.to_path_buf();
let renamed_path = path.with_file_name(&name);
relative_path = match relative_path.pop() {
true => {
relative_path.push(&name);
relative_path
}
false => PathBuf::from(&name),
};
let new_id = ItemId::with_index(name.clone(), id.get_index());
if self
.all_paths()
.iter()
.any(|(entry_id, entry_path)| entry_id != &id && *entry_path == &relative_path)
{
return Err(DatabaseError::IdAlreadyExists(new_id.as_string()));
}
fs::rename(&path, renamed_path)?;
self.remove_id_from_index(&id)?;
self.insert_path_for_id(&new_id, relative_path)?;
Ok(())
}
pub fn delete(
&mut self,
id: impl Into<ItemId>,
force: impl Into<bool>,
) -> Result<(), DatabaseError> {
let id = id.into();
let force = force.into();
if id.get_name().is_empty() {
match self.delete_directory(&self.locate_absolute(id)?, force) {
Ok(_) => {
self.path = PathBuf::new();
self.items.drain();
return Ok(());
}
Err(error) => return Err(error),
}
}
let path = self.locate_absolute(&id)?;
if path.is_dir() {
self.delete_directory(&path, force)?;
} else {
remove_file(path)?;
}
self.remove_id_from_index(&id)?;
Ok(())
}
pub fn locate_absolute(&self, id: impl Into<ItemId>) -> Result<PathBuf, DatabaseError> {
let id = id.into();
if id.get_name().is_empty() {
return Ok(self.path.to_path_buf());
}
Ok(self.path.join(self.resolve_path_by_id(&id)?))
}
pub fn locate_relative(&self, id: impl Into<ItemId>) -> Result<&PathBuf, DatabaseError> {
let id = id.into();
if id.get_name().is_empty() {
return Ok(&self.path);
}
self.resolve_path_by_id(&id)
}
pub fn get_ids_by_name(&self, name: impl AsRef<str>) -> Vec<ItemId> {
self.items
.get(name.as_ref())
.map(|paths| {
paths
.iter()
.map(|(index, _)| ItemId::with_index(name.as_ref(), index))
.collect()
})
.unwrap_or_default()
}
pub fn get_ids_by_index(&self, index: usize) -> Vec<ItemId> {
self.items
.iter()
.filter_map(|(name, paths)| {
paths
.get(index)
.map(|_| ItemId::with_index(name.clone(), index))
})
.collect()
}
pub fn scan_for_changes(
&mut self,
scan_from: impl Into<ItemId>,
policy: ScanPolicy,
recursive: bool,
) -> Result<ScanReport, DatabaseError> {
let scan_from = scan_from.into();
let scan_from_absolute = self.locate_absolute(&scan_from)?;
if !scan_from_absolute.is_dir() {
return Err(DatabaseError::NotADirectory(scan_from_absolute));
}
let scope_relative = if scan_from.get_name().is_empty() {
None
} else {
Some(self.locate_relative(&scan_from)?.clone())
};
let discovered_paths = self.collect_paths_in_scope(&scan_from_absolute, recursive)?;
let discovered_set: HashSet<PathBuf> = discovered_paths.iter().cloned().collect();
let mut existing_in_scope_set = HashSet::new();
let mut removed = Vec::new();
let mut unchanged_count = 0usize;
let mut removed_ids = Vec::new();
for (name, paths) in &self.items {
for (index, path) in paths.iter() {
if !self.is_path_in_scope(path, scope_relative.as_deref(), recursive) {
continue;
}
existing_in_scope_set.insert(path.clone());
let id = ItemId::with_index(name.clone(), index);
if discovered_set.contains(path) {
unchanged_count += 1;
} else {
removed.push(ExternalChange::Removed {
id: id.clone(),
path: path.clone(),
});
removed_ids.push(id);
}
}
}
for id in removed_ids {
let _ = self.remove_id_from_index(&id);
}
let mut added_paths: Vec<PathBuf> = discovered_paths
.into_iter()
.filter(|path| !existing_in_scope_set.contains(path))
.collect();
let mut added = Vec::new();
match policy {
ScanPolicy::DetectOnly => {
for path in &added_paths {
let name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or(DatabaseError::OsStringConversion)?
.to_string();
added.push(ExternalChange::Added {
id: ItemId::id(name),
path: path.clone(),
});
}
}
ScanPolicy::AddNew => {
for path in &added_paths {
let name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or(DatabaseError::OsStringConversion)?
.to_string();
let id = self.insert_generated_path(name, path.clone());
added.push(ExternalChange::Added {
id,
path: path.clone(),
});
}
}
ScanPolicy::RemoveNew => {
for path in &added_paths {
let name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or(DatabaseError::OsStringConversion)?
.to_string();
added.push(ExternalChange::Added {
id: ItemId::id(name),
path: path.clone(),
});
}
added_paths.sort_by_key(|path| std::cmp::Reverse(path.components().count()));
for path in added_paths {
let absolute = self.path.join(&path);
if !absolute.exists() {
continue;
}
if absolute.is_dir() {
remove_dir_all(&absolute)?;
} else if absolute.is_file() {
remove_file(&absolute)?;
}
}
}
}
let total_changed_count = added.len() + removed.len();
match policy {
ScanPolicy::RemoveNew => {
added.clear();
},
_ => (),
}
Ok(ScanReport {
scanned_from: scan_from,
recursive,
added,
removed,
unchanged_count,
total_changed_count,
})
}
pub fn migrate_database(&mut self, to: impl AsRef<Path>) -> Result<(), DatabaseError> {
let destination = to.as_ref().to_path_buf();
let name = self
.path
.file_name()
.ok_or_else(|| DatabaseError::NotADirectory(self.path.clone()))?;
let destination_database_path = destination.join(name);
if destination_database_path.exists() {
remove_dir_all(&destination_database_path)?;
}
self.copy_directory_recursive(&self.path, &destination_database_path)?;
remove_dir_all(&self.path)?;
self.path = destination_database_path;
Ok(())
}
pub fn migrate_item(
&mut self,
id: impl Into<ItemId>,
to: impl Into<ItemId>,
) -> Result<(), DatabaseError> {
let id = id.into();
let to = to.into();
if id.get_name().is_empty() {
return Err(DatabaseError::RootIdUnsupported);
}
let destination_dir = self.locate_absolute(&to)?;
if !destination_dir.is_dir() {
return Err(DatabaseError::NotADirectory(destination_dir));
}
let source_absolute = self.locate_absolute(&id)?;
let source_name = source_absolute
.file_name()
.ok_or_else(|| DatabaseError::NoMatchingID(id.as_string()))?;
let destination_absolute = destination_dir.join(source_name);
if destination_absolute == source_absolute {
return Err(DatabaseError::IdenticalSourceDestination(
destination_absolute,
));
}
if destination_absolute.exists() {
if destination_absolute.is_dir() {
remove_dir_all(&destination_absolute)?;
} else {
remove_file(&destination_absolute)?;
}
}
fs::rename(&source_absolute, &destination_absolute)?;
let relative_destination = destination_absolute.strip_prefix(&self.path)?.to_path_buf();
let source_name = relative_destination
.file_name()
.and_then(|name| name.to_str())
.ok_or(DatabaseError::OsStringConversion)?
.to_string();
let migrated_id = ItemId::with_index(source_name, id.get_index());
self.remove_id_from_index(&id)?;
self.insert_path_for_id(&migrated_id, relative_destination)?;
Ok(())
}
pub fn export_item(
&mut self,
id: impl Into<ItemId>,
to: impl AsRef<Path>,
mode: ExportMode,
) -> Result<(), DatabaseError> {
let id = id.into();
let destination_dir = {
let to = to.as_ref();
if to.is_absolute() {
to.to_path_buf()
} else {
current_dir()?.join(to)
}
};
if id.get_name().is_empty() {
return Err(DatabaseError::RootIdUnsupported);
}
if destination_dir.starts_with(&self.path) {
return Err(DatabaseError::ExportDestinationInsideDatabase(
destination_dir,
));
}
fs::create_dir_all(&destination_dir)?;
if !destination_dir.is_dir() {
return Err(DatabaseError::NotADirectory(destination_dir));
}
let source_absolute = self.locate_absolute(&id)?;
let source_name = source_absolute
.file_name()
.ok_or_else(|| DatabaseError::NoMatchingID(id.as_string()))?;
let destination_absolute = destination_dir.join(source_name);
if destination_absolute == source_absolute {
return Err(DatabaseError::IdenticalSourceDestination(
destination_absolute,
));
}
if destination_absolute.exists() {
if destination_absolute.is_dir() {
remove_dir_all(&destination_absolute)?;
} else {
remove_file(&destination_absolute)?;
}
}
match mode {
ExportMode::Copy => {
if source_absolute.is_dir() {
self.copy_directory_recursive(&source_absolute, &destination_absolute)?;
} else {
fs::copy(&source_absolute, &destination_absolute)?;
}
}
ExportMode::Move => {
match fs::rename(&source_absolute, &destination_absolute) {
Ok(_) => (),
Err(_) => {
if source_absolute.is_dir() {
self.copy_directory_recursive(&source_absolute, &destination_absolute)?;
remove_dir_all(&source_absolute)?;
} else {
fs::copy(&source_absolute, &destination_absolute)?;
remove_file(&source_absolute)?;
}
}
}
self.remove_id_from_index(&id)?;
}
}
Ok(())
}
pub fn import_item(
&mut self,
from: impl AsRef<Path>,
to: impl Into<ItemId>,
) -> Result<(), DatabaseError> {
let source_path = {
let from = from.as_ref();
if from.is_absolute() {
from.to_path_buf()
} else {
current_dir()?.join(from)
}
};
let to = to.into();
if source_path.starts_with(&self.path) {
return Err(DatabaseError::ImportSourceInsideDatabase(source_path));
}
let destination_parent = self.locate_absolute(&to)?;
if !destination_parent.is_dir() {
return Err(DatabaseError::NotADirectory(destination_parent));
}
let item_name = source_path
.file_name()
.ok_or_else(|| DatabaseError::NotAFile(source_path.clone()))?
.to_string_lossy()
.to_string();
let destination_absolute = destination_parent.join(&item_name);
let destination_relative = if to.get_name().is_empty() {
PathBuf::from(&item_name)
} else {
let mut relative = self.locate_relative(&to)?.to_path_buf();
relative.push(&item_name);
relative
};
if destination_absolute.exists()
|| self.path_exists_in_index(&destination_relative)
{
return Err(DatabaseError::IdAlreadyExists(item_name));
}
if source_path.is_dir() {
self.copy_directory_recursive(&source_path, &destination_absolute)?;
} else if source_path.is_file() {
fs::copy(&source_path, &destination_absolute)?;
} else {
return Err(DatabaseError::NoMatchingID(
source_path.display().to_string(),
));
}
let _id = self.insert_generated_path(item_name, destination_relative);
Ok(())
}
pub fn duplicate_item(
&mut self,
id: impl Into<ItemId>,
parent: impl Into<ItemId>,
name: impl AsRef<str>,
) -> Result<(), DatabaseError> {
let id = id.into();
let parent = parent.into();
let name = name.as_ref().to_owned();
if id.get_name().is_empty() {
return Err(DatabaseError::RootIdUnsupported);
}
let source_absolute = self.locate_absolute(&id)?;
let parent_absolute = self.locate_absolute(&parent)?;
if !parent_absolute.is_dir() {
return Err(DatabaseError::NotADirectory(parent_absolute));
}
let destination_absolute = parent_absolute.join(&name);
let destination_relative = if parent.get_name().is_empty() {
PathBuf::from(&name)
} else {
let mut path = self.locate_relative(&parent)?.to_path_buf();
path.push(&name);
path
};
if destination_absolute.exists()
|| self.path_exists_in_index(&destination_relative)
{
return Err(DatabaseError::IdAlreadyExists(name));
}
if source_absolute.is_dir() {
self.copy_directory_recursive(&source_absolute, &destination_absolute)?;
} else {
fs::copy(&source_absolute, &destination_absolute)?;
}
let duplicate_name = destination_relative
.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_default();
let _duplicate_id = self.insert_generated_path(duplicate_name, destination_relative);
Ok(())
}
pub fn get_file_information(
&self,
id: impl Into<ItemId>,
) -> Result<FileInformation, DatabaseError> {
let id = id.into();
let path = self.locate_absolute(id)?;
let metadata = fs::metadata(&path)?;
let name = {
let os = if path.is_dir() {
path.file_name()
} else {
path.file_stem()
};
os_str_to_string(os).ok()
};
let extension = {
if path.is_dir() {
None
} else {
os_str_to_string(path.extension()).ok()
}
};
let size = FileSize::from(metadata.len());
let unix_created = sys_time_to_unsigned_int(metadata.created());
let time_since_created = sys_time_to_time_since(metadata.created());
let unix_last_opened = sys_time_to_unsigned_int(metadata.accessed());
let time_since_last_opened = sys_time_to_time_since(metadata.accessed());
let unix_last_modified = sys_time_to_unsigned_int(metadata.modified());
let time_since_last_modified = sys_time_to_time_since(metadata.modified());
Ok(FileInformation {
name,
extension,
size,
unix_created,
time_since_created,
unix_last_opened,
time_since_last_opened,
unix_last_modified,
time_since_last_modified,
})
}
fn all_paths(&self) -> Vec<(ItemId, &PathBuf)> {
let mut result = Vec::new();
for (name, paths) in &self.items {
for (index, path) in paths.iter() {
result.push((ItemId::with_index(name.clone(), index), path));
}
}
result
}
fn path_exists_in_index(&self, relative_path: &Path) -> bool {
self.items
.values()
.any(|paths| paths.iter().any(|(_, path)| path == relative_path))
}
fn insert_path_for_id(&mut self, id: &ItemId, path: PathBuf) -> Result<(), DatabaseError> {
let paths = self.items.entry(id.get_name().to_string()).or_default();
if !paths.insert_at(id.get_index(), path) {
return Err(DatabaseError::IdAlreadyExists(id.as_string()));
}
Ok(())
}
fn insert_generated_path(&mut self, name: String, path: PathBuf) -> ItemId {
let paths = self.items.entry(name.clone()).or_default();
let index = paths.push(path);
ItemId::with_index(name, index)
}
fn remove_id_from_index(&mut self, id: &ItemId) -> Result<(), DatabaseError> {
let name = id.get_name().to_string();
let should_drop_name = {
let paths = self
.items
.get_mut(&name)
.ok_or_else(|| DatabaseError::NoMatchingID(id.as_string()))?;
if !paths.remove(id.get_index()) {
return Err(DatabaseError::NoMatchingID(id.as_string()));
}
paths.is_empty()
};
if should_drop_name {
self.items.remove(&name);
}
Ok(())
}
fn resolve_path_by_id(&self, id: &ItemId) -> Result<&PathBuf, DatabaseError> {
self.items
.get(id.get_name())
.and_then(|paths| paths.get(id.get_index()))
.ok_or_else(|| DatabaseError::NoMatchingID(id.as_string()))
}
fn overwrite_path_atomic_with<F>(&self, path: &Path, write_fn: F) -> Result<u64, DatabaseError>
where
F: FnOnce(&mut File) -> Result<u64, DatabaseError>,
{
if path.is_dir() {
return Err(DatabaseError::NotAFile(path.to_path_buf()));
}
let buffer = path.with_extension("tmp");
let result = (|| {
let mut file = File::create(&buffer)?;
let bytes_written = write_fn(&mut file)?;
file.sync_all()?;
fs::rename(&buffer, path)?;
Ok(bytes_written)
})();
if result.is_err() && buffer.exists() {
let _ = remove_file(&buffer);
}
result
}
fn collect_paths_in_scope(
&self,
scope_absolute: &Path,
recursive: bool,
) -> Result<Vec<PathBuf>, DatabaseError> {
let mut collected = Vec::new();
if recursive {
let mut stack = vec![scope_absolute.to_path_buf()];
while let Some(directory) = stack.pop() {
for entry in fs::read_dir(&directory)? {
let entry = entry?;
let absolute_path = entry.path();
let relative_path = absolute_path.strip_prefix(&self.path)?.to_path_buf();
if absolute_path.is_dir() {
collected.push(relative_path);
stack.push(absolute_path);
} else if absolute_path.is_file() {
collected.push(relative_path);
}
}
}
} else {
for entry in fs::read_dir(scope_absolute)? {
let entry = entry?;
let absolute_path = entry.path();
let relative_path = absolute_path.strip_prefix(&self.path)?.to_path_buf();
if absolute_path.is_dir() || absolute_path.is_file() {
collected.push(relative_path);
}
}
}
Ok(collected)
}
fn copy_directory_recursive(&self, from: &Path, to: &Path) -> Result<(), DatabaseError> {
fs::create_dir_all(to)?;
for entry in fs::read_dir(from)? {
let entry = entry?;
let source_path = entry.path();
let destination_path = to.join(entry.file_name());
if source_path.is_dir() {
self.copy_directory_recursive(&source_path, &destination_path)?;
} else {
fs::copy(&source_path, &destination_path)?;
}
}
Ok(())
}
fn is_path_in_scope(
&self,
path: &Path,
scope_relative: Option<&Path>,
recursive: bool,
) -> bool {
match scope_relative {
None => {
if recursive {
true
} else {
path.parent()
.is_some_and(|parent| parent.as_os_str().is_empty())
}
}
Some(scope_relative) => {
if recursive {
path.starts_with(scope_relative) && path != scope_relative
} else {
path.parent() == Some(scope_relative)
}
}
}
}
fn delete_directory(&self, path: &Path, force: bool) -> Result<(), DatabaseError> {
if force {
remove_dir_all(path)?;
} else {
remove_dir(path)?;
}
Ok(())
}
}
fn truncate(mut path: PathBuf, steps: i32) -> Result<PathBuf, DatabaseError> {
let parents = (path.ancestors().count() - 1) as i32;
if parents <= steps {
return Err(DatabaseError::PathStepOverflow(steps, parents));
}
for _ in 0..steps {
path.pop();
}
Ok(path)
}
fn os_str_to_string(os_str: Option<&OsStr>) -> Result<String, DatabaseError> {
let os_str = match os_str {
Some(os_str) => os_str,
None => return Err(DatabaseError::OsStringConversion),
};
match os_str.to_os_string().into_string() {
Ok(string) => Ok(string),
Err(_) => Err(DatabaseError::OsStringConversion),
}
}
fn sys_time_to_unsigned_int(time: io::Result<SystemTime>) -> Option<u64> {
match time {
Ok(time) => match time.duration_since(UNIX_EPOCH) {
Ok(duration) => Some(duration.as_secs()),
Err(_) => None,
},
Err(_) => None,
}
}
fn sys_time_to_time_since(time: io::Result<SystemTime>) -> Option<u64> {
let duration = match time {
Ok(time) => match SystemTime::now().duration_since(time) {
Ok(duration) => duration,
Err(_) => return None,
},
Err(_) => return None,
};
sys_time_to_unsigned_int(Ok(UNIX_EPOCH + duration))
}