Skip to main content

file_database/
lib.rs

1//! # Database
2//! Local file database utilities.
3//!
4//! This crate gives you a simple way to manage files and folders inside one database directory.
5//! The main type is **`DatabaseManager`**, and items are addressed with **`ItemId`**.
6//!
7//! ## How `ItemId` works
8//! - **`ItemId`** has a `name` and an `index`.
9//! - `name` is the shared key (for example `"test_file.txt"`).
10//! - `index` picks which path you want when that name exists more than once.
11//! - `ItemId::id("name")` always means index `0`.
12//! - `ItemId::database_id()` is the root ID for the database itself.
13//!
14//! This means duplicate names are allowed, and you can still target one exact item by index.
15//!
16//! # Example: Build `ItemId` values
17//! ```
18//! use file_database::ItemId;
19//!
20//! let first = ItemId::id("test_file.txt");
21//! let second = ItemId::with_index("test_file.txt", 1);
22//! let root = ItemId::database_id();
23//!
24//! assert_eq!(first.get_name(), "test_file.txt");
25//! assert_eq!(first.get_index(), 0);
26//! assert_eq!(second.get_index(), 1);
27//! assert_eq!(root.get_name(), "");
28//! assert_eq!(root.get_index(), 0);
29//! ```
30//!
31//! ## How to use **`GenPath`**
32//! **`GenPath`** is used to generate paths from a ruleset. This is primarily used as the root directory for **`DatabaseManager`**.
33//! Pick the method based on where your app starts:
34//! - `GenPath::from_working_dir(steps)` when your process starts in a useful working directory.
35//! - `GenPath::from_exe(steps)` when paths should be anchored to the executable location.
36//! - `GenPath::from_closest_match("name")` when you want to find the nearest matching folder
37//!   while walking upward from the executable.
38//!
39//! # Example: Build base paths with `GenPath`
40//! ```no_run
41//! use file_database::{DatabaseError, GenPath};
42//!
43//! fn main() -> Result<(), DatabaseError> {
44//!     let from_cwd = GenPath::from_working_dir(0)?;
45//!     let from_exe = GenPath::from_exe(0)?;
46//!     assert!(from_cwd.is_absolute() || from_cwd.is_relative());
47//!     assert!(from_exe.is_absolute() || from_exe.is_relative());
48//!     Ok(())
49//! }
50//! ```
51//!
52//! # Example: Find a folder by name with `GenPath`
53//! ```no_run
54//! use file_database::{DatabaseError, GenPath};
55//!
56//! fn main() -> Result<(), DatabaseError> {
57//!     let project_root = GenPath::from_closest_match("src")?;
58//!     assert!(project_root.ends_with("src"));
59//!     Ok(())
60//! }
61//! ```
62//!
63//! # Example: Create and overwrite a file
64//! ```no_run
65//! use file_database::{DatabaseError, DatabaseManager, ItemId};
66//!
67//! fn main() -> Result<(), DatabaseError> {
68//!     let mut manager = DatabaseManager::new(".", "database")?;
69//!     manager.write_new(ItemId::id("example.txt"), ItemId::database_id())?;
70//!     manager.overwrite_existing(ItemId::id("example.txt"), b"hello")?;
71//!     Ok(())
72//! }
73//! ```
74//!
75//! # Example: Duplicate names with indexes
76//! ```no_run
77//! use file_database::{DatabaseError, DatabaseManager, ItemId};
78//!
79//! fn main() -> Result<(), DatabaseError> {
80//!     let mut manager = DatabaseManager::new(".", "database")?;
81//!
82//!     manager.write_new(ItemId::id("folder_a"), ItemId::database_id())?;
83//!     manager.write_new(ItemId::id("folder_b"), ItemId::database_id())?;
84//!     manager.write_new(ItemId::id("test.txt"), ItemId::id("folder_a"))?;
85//!     manager.write_new(ItemId::id("test.txt"), ItemId::id("folder_b"))?;
86//!
87//!     // First match for "test.txt"
88//!     let first = ItemId::id("test.txt");
89//!     // Second match for "test.txt"
90//!     let second = ItemId::with_index("test.txt", 1);
91//!
92//!     let _first_path = manager.locate_absolute(first)?;
93//!     let _second_path = manager.locate_absolute(second)?;
94//!     Ok(())
95//! }
96//! ```
97//!
98//! # Example: Get all IDs for one shared name
99//! ```no_run
100//! use file_database::{DatabaseError, DatabaseManager, ItemId};
101//!
102//! fn main() -> Result<(), DatabaseError> {
103//!     let mut manager = DatabaseManager::new(".", "database")?;
104//!     manager.write_new(ItemId::id("a.txt"), ItemId::database_id())?;
105//!     manager.write_new(ItemId::id("folder"), ItemId::database_id())?;
106//!     manager.write_new(ItemId::id("a.txt"), ItemId::id("folder"))?;
107//!
108//!     let ids = manager.get_ids_from_shared_id(ItemId::id("a.txt"))?;
109//!     assert_eq!(ids.len(), 2);
110//!     assert_eq!(ids[0].get_index(), 0);
111//!     assert_eq!(ids[1].get_index(), 1);
112//!     Ok(())
113//! }
114//! ```
115
116use std::{
117    collections::{HashMap, HashSet},
118    env::{current_dir, current_exe},
119    ffi::OsStr,
120    fs::{self, File, create_dir, remove_dir, remove_dir_all, remove_file},
121    hash::Hash,
122    io::{self, Write},
123    path::{Path, PathBuf},
124    time::{SystemTime, UNIX_EPOCH},
125};
126use thiserror::Error;
127
128// Constants
129const ZERO: u64 = 0;
130const THOUSAND: u64 = 1_000;
131const MILLION: u64 = 1_000_000;
132const BILLION: u64 = 1_000_000_000;
133const TRILLION: u64 = 1_000_000_000_000;
134const QUADRILLION: u64 = 1_000_000_000_000_000;
135
136// -------- Enums --------
137#[derive(Debug, Error)]
138/// Errors returned by this library.
139pub enum DatabaseError {
140    /// Returned when requested path-step trimming exceeds the available path depth.
141    #[error("Steps '{0}' greater than path length '{1}'")]
142    PathStepOverflow(i32, i32),
143    /// Returned when no matching directory name can be found while walking upward.
144    #[error("Directory '{0}' not found along path to executable")]
145    NoClosestDir(String),
146    /// Returned when an `ItemId` name has no tracked entries in the index.
147    #[error("ID '{0}' doesn't point to a known path")]
148    NoMatchingID(String),
149    /// Returned when creating or renaming to an ID that already exists at the target path.
150    #[error("ID '{0}' already exists")]
151    IdAlreadyExists(String),
152    /// Returned when source and destination resolve to the same filesystem path.
153    #[error("Source and destination are identical: '{0}'")]
154    IdenticalSourceDestination(PathBuf),
155    /// Returned when an export destination points inside the managed database root.
156    #[error("Export destination is inside the database: '{0}'")]
157    ExportDestinationInsideDatabase(PathBuf),
158    /// Returned when an import source path points inside the managed database root.
159    #[error("Import source is inside the database: '{0}'")]
160    ImportSourceInsideDatabase(PathBuf),
161    /// Returned when the requested `index` is outside the bounds of the ID match list.
162    #[error("Index {index} out of bounds for ID '{id}' (len: {len})")]
163    IndexOutOfBounds {
164        id: String,
165        index: usize,
166        len: usize,
167    },
168    /// Returned when an operation does not allow `ItemId::database_id()` as input.
169    #[error("Root database ID cannot be used for this operation")]
170    RootIdUnsupported,
171    /// Returned when a path was expected to be a directory but is not.
172    #[error("Path '{0}' doesn't point to a directory")]
173    NotADirectory(PathBuf),
174    /// Returned when a path was expected to be a file but is not.
175    #[error("Path '{0}' doesn't point to a file")]
176    NotAFile(PathBuf),
177    /// Returned when converting an OS string/path segment into UTF-8 text fails.
178    #[error("Couldn't convert OsString to String")]
179    OsStringConversion,
180    /// Returned when an item has no parent inside the tracked database tree.
181    #[error("ID '{0}' doesn't have a parent")]
182    NoParent(String),
183    /// Returned when an underlying filesystem I/O operation fails.
184    #[error(transparent)]
185    Io(#[from] std::io::Error),
186    /// Returned when JSON serialization or deserialization fails.
187    #[error(transparent)]
188    SerdeJson(#[from] serde_json::Error),
189    /// Returned when bincode serialization or deserialization fails.
190    #[error(transparent)]
191    Bincode(#[from] bincode::Error),
192    /// Returned when converting an absolute path into a database-relative path fails.
193    #[error(transparent)]
194    PathBufConversion(#[from] std::path::StripPrefixError),
195}
196
197#[derive(Debug, PartialEq, Clone, Default)]
198/// Controls whether directory deletion is forced.
199pub enum ForceDeletion {
200    #[default]
201    Force,
202    NoForce,
203}
204
205impl Into<bool> for ForceDeletion {
206    /// Converts **`ForceDeletion`** into its boolean form.
207    fn into(self) -> bool {
208        match self {
209            ForceDeletion::Force => true,
210            ForceDeletion::NoForce => false,
211        }
212    }
213}
214
215impl From<bool> for ForceDeletion {
216    /// Converts a boolean into **`ForceDeletion`**.
217    fn from(value: bool) -> Self {
218        match value {
219            true => ForceDeletion::Force,
220            false => ForceDeletion::NoForce,
221        }
222    }
223}
224
225#[derive(Debug, PartialEq, Clone, Default)]
226/// Controls whether list results are sorted.
227pub enum ShouldSort {
228    #[default]
229    Sort,
230    NoSort,
231}
232
233impl Into<bool> for ShouldSort {
234    /// Converts **`ShouldSort`** into its boolean form.
235    fn into(self) -> bool {
236        match self {
237            ShouldSort::Sort => true,
238            ShouldSort::NoSort => false,
239        }
240    }
241}
242
243impl From<bool> for ShouldSort {
244    /// Converts a boolean into **`ShouldSort`**.
245    fn from(value: bool) -> Self {
246        match value {
247            true => ShouldSort::Sort,
248            false => ShouldSort::NoSort,
249        }
250    }
251}
252
253#[derive(Debug, PartialEq, Clone, Default)]
254/// Controls whether APIs should serialize values.
255pub enum Serialize {
256    #[default]
257    Serialize,
258    NoSerialize,
259}
260
261impl Into<bool> for Serialize {
262    /// Converts **`Serialize`** into its boolean form.
263    fn into(self) -> bool {
264        match self {
265            Serialize::Serialize => true,
266            Serialize::NoSerialize => false,
267        }
268    }
269}
270
271impl From<bool> for Serialize {
272    /// Converts a boolean into **`Serialize`**.
273    fn from(value: bool) -> Self {
274        match value {
275            true => Serialize::Serialize,
276            false => Serialize::NoSerialize,
277        }
278    }
279}
280
281#[derive(Debug, PartialEq, Clone, Default)]
282/// Controls whether export copies or moves the source.
283pub enum ExportMode {
284    #[default]
285    Copy,
286    Move,
287}
288
289#[derive(Debug, PartialEq, Clone, Default)]
290/// Controls how `scan_for_changes` handles newly found files.
291pub enum ScanPolicy {
292    DetectOnly,
293    RemoveNew,
294    #[default]
295    AddNew,
296}
297
298#[derive(Debug, Default, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)]
299/// Units used by **`FileSize`**.
300pub enum FileSizeUnit {
301    #[default]
302    Byte,
303    Kilobyte,
304    Megabyte,
305    Gigabyte,
306    Terabyte,
307    Petabyte,
308}
309
310impl FileSizeUnit {
311    /// Internal numeric rank used for unit conversion math.
312    fn variant_integer_id(&self) -> u8 {
313        match self {
314            Self::Byte => 0,
315            Self::Kilobyte => 1,
316            Self::Megabyte => 2,
317            Self::Gigabyte => 3,
318            Self::Terabyte => 4,
319            Self::Petabyte => 5,
320        }
321    }
322}
323
324// -------- Structs --------
325#[derive(PartialEq, Debug, Clone, Default)]
326/// Helper for building paths from the current process location.
327pub struct GenPath;
328
329impl GenPath {
330    /// Returns the working directory, with `steps` parts removed from the end.
331    ///
332    /// # Parameters
333    /// - `steps`: number of path parts at the end to remove.
334    ///
335    /// # Errors
336    /// Returns an error if:
337    /// - the current directory cannot be read,
338    /// - `steps` is greater than or equal to the number of removable segments.
339    ///
340    /// # Examples
341    /// ```no_run
342    /// use file_database::{DatabaseError, GenPath};
343    ///
344    /// fn main() -> Result<(), DatabaseError> {
345    ///     let _cwd = GenPath::from_working_dir(0)?;
346    ///     Ok(())
347    /// }
348    /// ```
349    pub fn from_working_dir(steps: i32) -> Result<PathBuf, DatabaseError> {
350        let working_dir = truncate(current_dir()?, steps)?;
351
352        Ok(working_dir)
353    }
354
355    /// Returns the executable directory, with `steps` parts removed from the end.
356    ///
357    /// # Parameters
358    /// - `steps`: number of path parts at the end to remove from the executable directory.
359    ///
360    /// # Errors
361    /// Returns an error if:
362    /// - the executable path cannot be read,
363    /// - `steps` is too large for the path depth.
364    ///
365    /// # Examples
366    /// ```no_run
367    /// use file_database::{DatabaseError, GenPath};
368    ///
369    /// fn main() -> Result<(), DatabaseError> {
370    ///     let _exe_dir = GenPath::from_exe(0)?;
371    ///     Ok(())
372    /// }
373    /// ```
374    pub fn from_exe(steps: i32) -> Result<PathBuf, DatabaseError> {
375        let exe = truncate(current_exe()?, steps + 1)?;
376
377        Ok(exe)
378    }
379
380    /// Looks for the nearest matching folder name while walking up from the executable.
381    ///
382    /// At each level, this checks:
383    /// - the folder name itself,
384    /// - child folders one level down.
385    ///
386    /// File entries are ignored.
387    ///
388    /// # Parameters
389    /// - `name`: directory name to search for.
390    ///
391    /// # Errors
392    /// Returns an error if:
393    /// - no matching directory is found,
394    /// - the provided name cannot be converted from `OsStr` to `String`.
395    ///
396    /// # Examples
397    /// ```no_run
398    /// use file_database::{DatabaseError, GenPath};
399    ///
400    /// fn main() -> Result<(), DatabaseError> {
401    ///     let _path = GenPath::from_closest_match("src")?;
402    ///     Ok(())
403    /// }
404    /// ```
405    pub fn from_closest_match(name: impl AsRef<Path>) -> Result<PathBuf, DatabaseError> {
406        let exe = current_exe()?;
407        let target = name.as_ref();
408        let target_name = target.as_os_str();
409
410        for path in exe.ancestors() {
411            if !path.is_dir() {
412                continue;
413            }
414
415            if path
416                .file_name()
417                .is_some_and(|dir_name| dir_name == target_name)
418            {
419                return Ok(path.to_path_buf());
420            }
421
422            if let Ok(entries) = fs::read_dir(path) {
423                for entry in entries {
424                    let entry = match entry {
425                        Ok(entry) => entry,
426                        Err(_) => continue,
427                    };
428
429                    let child_path = entry.path();
430                    if !child_path.is_dir() {
431                        continue;
432                    }
433
434                    if entry.file_name() == target_name {
435                        return Ok(child_path);
436                    }
437                }
438            }
439        }
440
441        let name_as_string = match target.to_owned().into_os_string().into_string() {
442            Ok(string) => string,
443            Err(_) => return Err(DatabaseError::OsStringConversion),
444        };
445
446        Err(DatabaseError::NoClosestDir(name_as_string))
447    }
448}
449
450#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd, Ord)]
451/// Identifier used to select a tracked item by `name` and `index`.
452///
453/// Use this when:
454/// - you know the shared `name` and want the first match (`ItemId::id("name")`),
455/// - or when you need a specific duplicate (`ItemId::with_index("name", i)`).
456///
457/// `ItemId::database_id()` is special and points to the database root itself.
458///
459/// # Examples
460/// ```
461/// use file_database::ItemId;
462///
463/// let first = ItemId::id("report.txt");
464/// let second = ItemId::with_index("report.txt", 1);
465/// let root = ItemId::database_id();
466///
467/// assert_eq!(first.get_name(), "report.txt");
468/// assert_eq!(first.get_index(), 0);
469/// assert_eq!(second.get_index(), 1);
470/// assert_eq!(root.get_name(), "");
471/// ```
472pub struct ItemId {
473    name: String,
474    index: usize,
475}
476
477impl<T> From<T> for ItemId
478where
479    T: Into<String>,
480{
481    /// Creates an **`ItemId`** from a name, defaulting `index` to `0`.
482    fn from(value: T) -> Self {
483        Self {
484            name: value.into(),
485            index: 0,
486        }
487    }
488}
489
490impl From<&ItemId> for ItemId {
491    /// Clones an **`ItemId`** from a reference.
492    fn from(value: &ItemId) -> Self {
493        value.clone()
494    }
495}
496
497impl ItemId {
498    /// Returns the `ItemId::database_id()` for the database itself.
499    ///
500    /// # Examples
501    /// ```
502    /// use file_database::ItemId;
503    ///
504    /// let root = ItemId::database_id();
505    /// assert_eq!(root.get_name(), "");
506    /// assert_eq!(root.get_index(), 0);
507    /// ```
508    pub fn database_id() -> Self {
509        Self {
510            name: String::new(),
511            index: 0,
512        }
513    }
514
515    /// Creates an **`ItemId`** with `index` `0`.
516    ///
517    /// # Parameters
518    /// - `id`: shared `name` key stored in the manager.
519    ///
520    /// # Examples
521    /// ```
522    /// use file_database::ItemId;
523    ///
524    /// let id = ItemId::id("file.txt");
525    /// assert_eq!(id.get_name(), "file.txt");
526    /// assert_eq!(id.get_index(), 0);
527    /// ```
528    pub fn id(id: impl Into<String>) -> Self {
529        Self {
530            name: id.into(),
531            index: 0,
532        }
533    }
534
535    /// Creates an **`ItemId`** with an explicit shared-name `index`.
536    ///
537    /// # Parameters
538    /// - `id`: shared `name` key.
539    /// - `index`: zero-based `index` within that key's stored path vector.
540    ///
541    /// # Examples
542    /// ```
543    /// use file_database::ItemId;
544    ///
545    /// let id = ItemId::with_index("file.txt", 2);
546    /// assert_eq!(id.get_name(), "file.txt");
547    /// assert_eq!(id.get_index(), 2);
548    /// ```
549    pub fn with_index(id: impl Into<String>, index: usize) -> Self {
550        Self {
551            name: id.into(),
552            index,
553        }
554    }
555
556    /// Returns the shared `name` of this **`ItemId`**.
557    pub fn get_name(&self) -> &str {
558        &self.name
559    }
560
561    /// Returns the zero-based `index` for this shared `name`.
562    pub fn get_index(&self) -> usize {
563        self.index
564    }
565
566    /// Returns the shared `name` as `&str`.
567    pub fn as_str(&self) -> &str {
568        self.get_name()
569    }
570
571    /// Returns an owned `String` containing this **`ItemId`**'s shared `name`.
572    pub fn as_string(&self) -> String {
573        self.name.clone()
574    }
575}
576
577#[derive(Debug, Default, PartialEq, PartialOrd, Clone, Copy)]
578/// File size value paired with a unit.
579pub struct FileSize {
580    size: u64,
581    unit: FileSizeUnit,
582}
583
584impl FileSize {
585    /// Returns the stored size value in the current unit.
586    pub fn get_size(&self) -> u64 {
587        self.size
588    }
589
590    /// Returns the stored unit.
591    pub fn get_unit(&self) -> FileSizeUnit {
592        self.unit
593    }
594
595    /// Returns a human-readable unit string, pluralized when needed.
596    ///
597    /// # Examples
598    /// ```
599    /// use file_database::FileSize;
600    ///
601    /// let size = FileSize::default();
602    /// assert_eq!(size.unit_as_string(), "Bytes");
603    /// ```
604    pub fn unit_as_string(&self) -> String {
605        let name = match self.unit {
606            FileSizeUnit::Byte => "Byte",
607            FileSizeUnit::Kilobyte => "Kilobyte",
608            FileSizeUnit::Megabyte => "Megabyte",
609            FileSizeUnit::Gigabyte => "Gigabyte",
610            FileSizeUnit::Terabyte => "Terabyte",
611            FileSizeUnit::Petabyte => "Petabyte",
612        };
613
614        let mut name_string = String::from(name);
615
616        // Push an s to the end of the string if not 1
617        match self.size {
618            1 => (),
619            _ => name_string.push('s'),
620        }
621
622        name_string
623    }
624
625    /// Returns a copy of this size converted to another unit.
626    ///
627    /// Conversion uses powers of 1000 between adjacent units.
628    ///
629    /// # Parameters
630    /// - `unit`: destination unit.
631    ///
632    /// # Examples
633    /// ```
634    /// use file_database::{FileSize, FileSizeUnit};
635    ///
636    /// let bytes = FileSize::default().as_unit(FileSizeUnit::Byte);
637    /// assert_eq!(bytes.get_unit(), FileSizeUnit::Byte);
638    /// ```
639    pub fn as_unit(&self, unit: FileSizeUnit) -> Self {
640        let difference = self.unit.variant_integer_id() as i8 - unit.variant_integer_id() as i8;
641
642        let mut size = self.size;
643
644        if difference > 0 {
645            let factor = THOUSAND.pow(difference as u32);
646            size = size.saturating_mul(factor);
647        } else if difference < 0 {
648            let factor = THOUSAND.pow((-difference) as u32);
649            size /= factor;
650        }
651
652        Self { size, unit }
653    }
654
655    /// Builds **`FileSize`** from raw bytes using automatic unit selection.
656    fn from(bytes: u64) -> Self {
657        let (size, unit) = match bytes {
658            ZERO..THOUSAND => (bytes, FileSizeUnit::Byte),
659            THOUSAND..MILLION => (bytes / THOUSAND, FileSizeUnit::Kilobyte),
660            MILLION..BILLION => (bytes / MILLION, FileSizeUnit::Megabyte),
661            BILLION..TRILLION => (bytes / BILLION, FileSizeUnit::Gigabyte),
662            TRILLION..QUADRILLION => (bytes / TRILLION, FileSizeUnit::Terabyte),
663            _ => (bytes / QUADRILLION, FileSizeUnit::Petabyte),
664        };
665
666        Self { size, unit }
667    }
668}
669
670#[derive(Debug, Default, PartialEq, PartialOrd, Clone)]
671/// Metadata returned by `get_file_information`.
672pub struct FileInformation {
673    name: Option<String>,
674    extension: Option<String>,
675    size: FileSize,
676    unix_created: Option<u64>,
677    time_since_created: Option<u64>,
678    unix_last_opened: Option<u64>,
679    time_since_last_opened: Option<u64>,
680    unix_last_modified: Option<u64>,
681    time_since_last_modified: Option<u64>,
682}
683
684impl FileInformation {
685    /// Returns file `name` without extension, or directory `name` for directories.
686    pub fn get_name(&self) -> Option<&str> {
687        self.name.as_deref()
688    }
689
690    /// Returns file extension for files, otherwise `None`.
691    pub fn get_extension(&self) -> Option<&str> {
692        self.extension.as_deref()
693    }
694
695    /// Returns normalized file size data.
696    pub fn get_size(&self) -> &FileSize {
697        &self.size
698    }
699
700    /// Returns created-at Unix timestamp (seconds), when available on this platform.
701    pub fn get_unix_created(&self) -> Option<&u64> {
702        self.unix_created.as_ref()
703    }
704
705    /// Returns age since creation in seconds, when available.
706    pub fn get_time_since_created(&self) -> Option<&u64> {
707        self.time_since_created.as_ref()
708    }
709
710    /// Returns last-accessed Unix timestamp (seconds), when available.
711    pub fn get_unix_last_opened(&self) -> Option<&u64> {
712        self.unix_last_opened.as_ref()
713    }
714
715    /// Returns age since last access in seconds, when available.
716    pub fn get_time_since_last_opened(&self) -> Option<&u64> {
717        self.time_since_last_opened.as_ref()
718    }
719
720    /// Returns last-modified Unix timestamp (seconds), when available.
721    pub fn get_unix_last_modified(&self) -> Option<&u64> {
722        self.unix_last_modified.as_ref()
723    }
724
725    /// Returns age since last modification in seconds, when available.
726    pub fn get_time_since_last_modified(&self) -> Option<&u64> {
727        self.time_since_last_modified.as_ref()
728    }
729}
730
731#[derive(Debug, PartialEq, Clone)]
732/// A file or folder change found by `scan_for_changes`.
733pub enum ExternalChange {
734    Added { id: ItemId, path: PathBuf },
735    Removed { id: ItemId, path: PathBuf },
736}
737
738#[derive(Debug, PartialEq, Clone)]
739/// Summary returned by `scan_for_changes`.
740pub struct ScanReport {
741    scanned_from: ItemId,
742    recursive: bool,
743    added: Vec<ExternalChange>,
744    removed: Vec<ExternalChange>,
745    unchanged_count: usize,
746    total_changed_count: usize,
747}
748
749impl ScanReport {
750    /// Returns the **`ItemId`** used as the scan root.
751    pub fn get_scan_from(&self) -> &ItemId {
752        &self.scanned_from
753    }
754
755    /// Returns all newly discovered items in the scanned scope.
756    pub fn get_added(&self) -> &Vec<ExternalChange> {
757        &self.added
758    }
759
760    /// Returns tracked **`ItemId`** values that were missing on disk.
761    pub fn get_removed(&self) -> &Vec<ExternalChange> {
762        &self.removed
763    }
764
765    /// Returns how many tracked **`ItemId`** values stayed the same in this scan area.
766    pub fn get_unchanged_count(&self) -> usize {
767        self.unchanged_count
768    }
769
770    /// Returns total number of changed items (`added + removed`).
771    pub fn get_total_changed_count(&self) -> usize {
772        self.total_changed_count
773    }
774}
775
776#[derive(Debug, PartialEq)]
777/// Main type that manages a database directory and its index.
778pub struct DatabaseManager {
779    path: PathBuf,
780    items: HashMap<String, Vec<PathBuf>>,
781}
782
783impl DatabaseManager {
784    /// Creates a new database directory and returns a manager for it.
785    ///
786    /// # Parameters
787    /// - `path`: parent directory where the database folder will be created.
788    /// - `name`: database directory name appended to `path`.
789    ///
790    /// # Errors
791    /// Returns an error if:
792    /// - the destination directory already exists,
793    /// - parent directories are missing,
794    /// - the process cannot create directories at the destination.
795    ///
796    /// # Examples
797    /// ```no_run
798    /// use file_database::{DatabaseError, DatabaseManager};
799    ///
800    /// fn main() -> Result<(), DatabaseError> {
801    ///     let _manager = DatabaseManager::new(".", "database")?;
802    ///     Ok(())
803    /// }
804    /// ```
805    pub fn new(path: impl AsRef<Path>, name: impl AsRef<Path>) -> Result<Self, DatabaseError> {
806        let mut path: PathBuf = path.as_ref().to_path_buf();
807
808        path.push(name);
809
810        create_dir(&path)?;
811
812        let manager = Self {
813            path: path.into(),
814            items: HashMap::new(),
815        };
816
817        Ok(manager)
818    }
819
820    /// Creates a new file or directory under `parent`.
821    ///
822    /// Name interpretation is extension-based:
823    /// - if `id.name` has an extension, a file is created,
824    /// - otherwise, a directory is created.
825    ///
826    /// # Parameters
827    /// - `id`: name key for the new item. Root **`ItemId`** is not allowed.
828    /// - `parent`: destination parent item. Use `ItemId::database_id()` for database root.
829    ///
830    /// # Errors
831    /// Returns an error if:
832    /// - `id` is the `ItemId::database_id()`,
833    /// - `parent` cannot be found,
834    /// - another item already exists at the target relative path,
835    /// - filesystem create operations fail.
836    ///
837    /// # Examples
838    /// ```no_run
839    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
840    ///
841    /// fn main() -> Result<(), DatabaseError> {
842    ///     let mut manager = DatabaseManager::new(".", "database")?;
843    ///     manager.write_new(ItemId::id("notes.txt"), ItemId::database_id())?;
844    ///     Ok(())
845    /// }
846    /// ```
847    pub fn write_new(
848        &mut self,
849        id: impl Into<ItemId>,
850        parent: impl Into<ItemId>,
851    ) -> Result<(), DatabaseError> {
852        let id = id.into();
853        let parent = parent.into();
854
855        if id.get_name().is_empty() {
856            return Err(DatabaseError::RootIdUnsupported);
857        }
858
859        let absolute_parent_path = self.locate_absolute(&parent)?;
860        let relative_path = if parent.get_name().is_empty() {
861            PathBuf::from(id.get_name())
862        } else {
863            let mut path = self.locate_relative(parent)?.to_path_buf();
864            path.push(id.get_name());
865            path
866        };
867        let absolute_path = absolute_parent_path.join(id.get_name());
868
869        if self
870            .items
871            .get(id.get_name())
872            .is_some_and(|paths| paths.iter().any(|path| path == &relative_path))
873        {
874            return Err(DatabaseError::IdAlreadyExists(id.as_string()));
875        }
876
877        if relative_path.extension().is_none() {
878            create_dir(&absolute_path)?;
879        } else {
880            File::create_new(&absolute_path)?;
881        }
882
883        self.items
884            .entry(id.get_name().to_string())
885            .or_default()
886            .push(relative_path);
887        Ok(())
888    }
889
890    /// Overwrites an existing file with raw bytes in a safe way.
891    ///
892    /// It writes to a temp file first, then replaces the target file.
893    ///
894    /// # Parameters
895    /// - `id`: target file **`ItemId`**.
896    /// - `data`: raw bytes to write.
897    ///
898    /// # Errors
899    /// Returns an error if:
900    /// - `id` cannot be found,
901    /// - `id` points to a directory,
902    /// - writing, syncing, or renaming fails.
903    ///
904    /// # Examples
905    /// ```no_run
906    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
907    ///
908    /// fn main() -> Result<(), DatabaseError> {
909    ///     let mut manager = DatabaseManager::new(".", "database")?;
910    ///     manager.write_new(ItemId::id("blob.bin"), ItemId::database_id())?;
911    ///     manager.overwrite_existing(ItemId::id("blob.bin"), [1_u8, 2, 3, 4])?;
912    ///     Ok(())
913    /// }
914    /// ```
915    pub fn overwrite_existing<T>(&self, id: impl Into<ItemId>, data: T) -> Result<(), DatabaseError>
916    where
917        T: AsRef<[u8]>,
918    {
919        let id = id.into();
920        let bytes = data.as_ref();
921
922        let path = self.locate_absolute(id)?;
923
924        self.overwrite_path_atomic_with(&path, |file| {
925            file.write_all(bytes)?;
926            Ok(bytes.len() as u64)
927        })?;
928
929        Ok(())
930    }
931
932    /// Converts `value` to JSON and overwrites the target file.
933    ///
934    /// # Parameters
935    /// - `id`: target file **`ItemId`**.
936    /// - `value`: serializable value.
937    ///
938    /// # Errors
939    /// Returns an error if:
940    /// - JSON serialization fails,
941    /// - finding `id` or overwriting the file fails.
942    ///
943    /// # Examples
944    /// ```no_run
945    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
946    /// use serde::Serialize;
947    ///
948    /// #[derive(Serialize)]
949    /// struct Config {
950    ///     retries: u8,
951    /// }
952    ///
953    /// fn main() -> Result<(), DatabaseError> {
954    ///     let mut manager = DatabaseManager::new(".", "database")?;
955    ///     manager.write_new(ItemId::id("config.json"), ItemId::database_id())?;
956    ///     manager.overwrite_existing_json(ItemId::id("config.json"), &Config { retries: 3 })?;
957    ///     Ok(())
958    /// }
959    /// ```
960    pub fn overwrite_existing_json<T: serde::Serialize>(
961        &self,
962        id: impl Into<ItemId>,
963        value: &T,
964    ) -> Result<(), DatabaseError> {
965        let data = serde_json::to_vec(value)?;
966        self.overwrite_existing(id, data)
967    }
968
969    /// Converts `value` to bincode and overwrites the target file.
970    ///
971    /// # Parameters
972    /// - `id`: target file **`ItemId`**.
973    /// - `value`: serializable value.
974    ///
975    /// # Errors
976    /// Returns an error if:
977    /// - bincode serialization fails,
978    /// - finding `id` or overwriting the file fails.
979    ///
980    /// # Examples
981    /// ```no_run
982    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
983    /// use serde::Serialize;
984    ///
985    /// #[derive(Serialize)]
986    /// enum State {
987    ///     Ready,
988    /// }
989    ///
990    /// fn main() -> Result<(), DatabaseError> {
991    ///     let mut manager = DatabaseManager::new(".", "database")?;
992    ///     manager.write_new(ItemId::id("state.bin"), ItemId::database_id())?;
993    ///     manager.overwrite_existing_binary(ItemId::id("state.bin"), &State::Ready)?;
994    ///     Ok(())
995    /// }
996    /// ```
997    pub fn overwrite_existing_binary<T: serde::Serialize>(
998        &self,
999        id: impl Into<ItemId>,
1000        value: &T,
1001    ) -> Result<(), DatabaseError> {
1002        let data = bincode::serialize(value)?;
1003        self.overwrite_existing(id, data)
1004    }
1005
1006    /// Streams bytes from `reader` into the target file and returns bytes written.
1007    ///
1008    /// This uses chunked I/O and a safe replace step, so it works well for large payloads.
1009    ///
1010    /// # Parameters
1011    /// - `id`: target file **`ItemId`**.
1012    /// - `reader`: source stream consumed until EOF.
1013    ///
1014    /// # Errors
1015    /// Returns an error if:
1016    /// - `id` cannot be found,
1017    /// - target is not a file,
1018    /// - stream read/write/sync/rename fails.
1019    ///
1020    /// # Examples
1021    /// ```no_run
1022    /// use std::io::Cursor;
1023    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1024    ///
1025    /// fn main() -> Result<(), DatabaseError> {
1026    ///     let mut manager = DatabaseManager::new(".", "database")?;
1027    ///     manager.write_new(ItemId::id("stream.bin"), ItemId::database_id())?;
1028    ///     let mut source = Cursor::new(vec![9_u8; 1024]);
1029    ///     let _bytes = manager.overwrite_existing_from_reader(ItemId::id("stream.bin"), &mut source)?;
1030    ///     Ok(())
1031    /// }
1032    /// ```
1033    pub fn overwrite_existing_from_reader<R: io::Read>(
1034        &self,
1035        id: impl Into<ItemId>,
1036        reader: &mut R,
1037    ) -> Result<u64, DatabaseError> {
1038        let id = id.into();
1039        let path = self.locate_absolute(id)?;
1040        self.overwrite_path_atomic_with(&path, |file| Ok(io::copy(reader, file)?))
1041    }
1042
1043    /// Reads a managed file and returns its raw bytes.
1044    ///
1045    /// # Parameters
1046    /// - `id`: target file **`ItemId`**.
1047    ///
1048    /// # Errors
1049    /// Returns an error if:
1050    /// - `id` cannot be found,
1051    /// - `id` points to a directory,
1052    /// - file reading fails.
1053    ///
1054    /// # Examples
1055    /// ```no_run
1056    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1057    ///
1058    /// fn main() -> Result<(), DatabaseError> {
1059    ///     let mut manager = DatabaseManager::new(".", "database")?;
1060    ///     manager.write_new(ItemId::id("data.bin"), ItemId::database_id())?;
1061    ///     manager.overwrite_existing(ItemId::id("data.bin"), [1_u8, 2, 3])?;
1062    ///     let _data = manager.read_existing(ItemId::id("data.bin"))?;
1063    ///     Ok(())
1064    /// }
1065    /// ```
1066    pub fn read_existing(&self, id: impl Into<ItemId>) -> Result<Vec<u8>, DatabaseError> {
1067        let id = id.into();
1068        let path = self.locate_absolute(id)?;
1069
1070        if path.is_dir() {
1071            return Err(DatabaseError::NotAFile(path));
1072        }
1073
1074        Ok(fs::read(path)?)
1075    }
1076
1077    /// Reads a managed file and turns JSON into `T`.
1078    ///
1079    /// # Parameters
1080    /// - `id`: target file **`ItemId`**.
1081    ///
1082    /// # Errors
1083    /// Returns an error if:
1084    /// - finding `id` or reading the file fails,
1085    /// - JSON deserialization fails.
1086    ///
1087    /// # Examples
1088    /// ```no_run
1089    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1090    /// use serde::{Deserialize, Serialize};
1091    ///
1092    /// #[derive(Serialize, Deserialize)]
1093    /// struct Config {
1094    ///     retries: u8,
1095    /// }
1096    ///
1097    /// fn main() -> Result<(), DatabaseError> {
1098    ///     let mut manager = DatabaseManager::new(".", "database")?;
1099    ///     manager.write_new(ItemId::id("config.json"), ItemId::database_id())?;
1100    ///     manager.overwrite_existing_json(ItemId::id("config.json"), &Config { retries: 3 })?;
1101    ///     let _loaded: Config = manager.read_existing_json(ItemId::id("config.json"))?;
1102    ///     Ok(())
1103    /// }
1104    /// ```
1105    pub fn read_existing_json<T: serde::de::DeserializeOwned>(
1106        &self,
1107        id: impl Into<ItemId>,
1108    ) -> Result<T, DatabaseError> {
1109        let bytes = self.read_existing(id)?;
1110        Ok(serde_json::from_slice(&bytes)?)
1111    }
1112
1113    /// Reads a managed file and turns bincode into `T`.
1114    ///
1115    /// # Parameters
1116    /// - `id`: target file **`ItemId`**.
1117    ///
1118    /// # Errors
1119    /// Returns an error if:
1120    /// - finding `id` or reading the file fails,
1121    /// - bincode deserialization fails.
1122    ///
1123    /// # Examples
1124    /// ```no_run
1125    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1126    /// use serde::{Deserialize, Serialize};
1127    ///
1128    /// #[derive(Serialize, Deserialize)]
1129    /// enum State {
1130    ///     Ready,
1131    /// }
1132    ///
1133    /// fn main() -> Result<(), DatabaseError> {
1134    ///     let mut manager = DatabaseManager::new(".", "database")?;
1135    ///     manager.write_new(ItemId::id("state.bin"), ItemId::database_id())?;
1136    ///     manager.overwrite_existing_binary(ItemId::id("state.bin"), &State::Ready)?;
1137    ///     let _loaded: State = manager.read_existing_binary(ItemId::id("state.bin"))?;
1138    ///     Ok(())
1139    /// }
1140    /// ```
1141    pub fn read_existing_binary<T: serde::de::DeserializeOwned>(
1142        &self,
1143        id: impl Into<ItemId>,
1144    ) -> Result<T, DatabaseError> {
1145        let bytes = self.read_existing(id)?;
1146        Ok(bincode::deserialize(&bytes)?)
1147    }
1148
1149    /// Returns every tracked item in the database.
1150    ///
1151    /// # Parameters
1152    /// - `sorted`: whether output should be sorted by **`ItemId`** ordering.
1153    ///
1154    /// # Examples
1155    /// ```no_run
1156    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1157    ///
1158    /// fn main() -> Result<(), DatabaseError> {
1159    ///     let mut manager = DatabaseManager::new(".", "database")?;
1160    ///     manager.write_new(ItemId::id("a.txt"), ItemId::database_id())?;
1161    ///     let _all = manager.get_all(true);
1162    ///     Ok(())
1163    /// }
1164    /// ```
1165    pub fn get_all(&self, sorted: impl Into<bool>) -> Vec<ItemId> {
1166        let sorted = sorted.into();
1167
1168        let mut list: Vec<ItemId> = self
1169            .items
1170            .iter()
1171            .flat_map(|(name, paths)| {
1172                paths
1173                    .iter()
1174                    .enumerate()
1175                    .map(|(index, _)| ItemId::with_index(name.clone(), index))
1176            })
1177            .collect();
1178
1179        if sorted {
1180            list.sort();
1181        }
1182
1183        list
1184    }
1185
1186    /// Returns all tracked items that are direct children of `parent`.
1187    ///
1188    /// If `parent` is the `ItemId::database_id()`, this returns all top-level items.
1189    ///
1190    /// # Parameters
1191    /// - `parent`: parent directory item to query.
1192    /// - `sorted`: whether output should be sorted by **`ItemId`**.
1193    ///
1194    /// # Errors
1195    /// Returns an error if:
1196    /// - `parent` cannot be found,
1197    /// - `parent` points to a file instead of a directory.
1198    ///
1199    /// # Examples
1200    /// ```no_run
1201    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1202    ///
1203    /// fn main() -> Result<(), DatabaseError> {
1204    ///     let mut manager = DatabaseManager::new(".", "database")?;
1205    ///     manager.write_new(ItemId::id("folder"), ItemId::database_id())?;
1206    ///     manager.write_new(ItemId::id("a.txt"), ItemId::id("folder"))?;
1207    ///     let _children = manager.get_by_parent(ItemId::id("folder"), true)?;
1208    ///     Ok(())
1209    /// }
1210    /// ```
1211    pub fn get_by_parent(
1212        &self,
1213        parent: impl Into<ItemId>,
1214        sorted: impl Into<bool>,
1215    ) -> Result<Vec<ItemId>, DatabaseError> {
1216        let parent = parent.into();
1217        let sorted = sorted.into();
1218
1219        let absolute_parent = self.locate_absolute(&parent)?;
1220
1221        if !absolute_parent.is_dir() {
1222            return Err(DatabaseError::NotADirectory(absolute_parent));
1223        }
1224
1225        let mut list: Vec<ItemId> = if parent.get_name().is_empty() {
1226            self.items
1227                .iter()
1228                .flat_map(|(name, paths)| {
1229                    paths.iter().enumerate().filter_map(|(index, item_path)| {
1230                        item_path
1231                            .parent()
1232                            .is_some_and(|parent| parent.as_os_str().is_empty())
1233                            .then_some(ItemId::with_index(name.clone(), index))
1234                    })
1235                })
1236                .collect()
1237        } else {
1238            let parent_path = self.locate_relative(parent)?;
1239            self.items
1240                .iter()
1241                .flat_map(|(name, paths)| {
1242                    paths.iter().enumerate().filter_map(|(index, item_path)| {
1243                        (item_path.parent() == Some(parent_path.as_path()))
1244                            .then_some(ItemId::with_index(name.clone(), index))
1245                    })
1246                })
1247                .collect()
1248        };
1249
1250        if sorted {
1251            list.sort();
1252        }
1253
1254        Ok(list)
1255    }
1256
1257    /// Returns the parent **`ItemId`** for an item.
1258    ///
1259    /// Top-level items return [`ItemId::database_id`].
1260    ///
1261    /// # Parameters
1262    /// - `id`: item whose parent should be looked up.
1263    ///
1264    /// # Errors
1265    /// Returns an error if:
1266    /// - `id` cannot be found,
1267    /// - parent path data cannot be converted to UTF-8 string.
1268    ///
1269    /// # Examples
1270    /// ```no_run
1271    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1272    ///
1273    /// fn main() -> Result<(), DatabaseError> {
1274    ///     let mut manager = DatabaseManager::new(".", "database")?;
1275    ///     manager.write_new(ItemId::id("folder"), ItemId::database_id())?;
1276    ///     manager.write_new(ItemId::id("a.txt"), ItemId::id("folder"))?;
1277    ///     let _parent = manager.get_parent(ItemId::id("a.txt"))?;
1278    ///     Ok(())
1279    /// }
1280    /// ```
1281    pub fn get_parent(&self, id: impl Into<ItemId>) -> Result<ItemId, DatabaseError> {
1282        let id = id.into();
1283        let path = self.locate_relative(&id)?;
1284
1285        let parent = match path.parent() {
1286            Some(parent) => parent,
1287            None => return Ok(ItemId::database_id()),
1288        };
1289
1290        if parent.as_os_str().is_empty() {
1291            return Ok(ItemId::database_id());
1292        }
1293
1294        match parent.file_name() {
1295            Some(name) => Ok(ItemId::id(os_str_to_string(Some(name))?)),
1296            None => Err(DatabaseError::NoParent(id.as_string())),
1297        }
1298    }
1299
1300    /// Renames the chosen item to `to` in the same parent directory.
1301    ///
1302    /// # Parameters
1303    /// - `id`: source **`ItemId`** to rename.
1304    /// - `to`: new file or directory name.
1305    ///
1306    /// # Errors
1307    /// Returns an error if:
1308    /// - `id` is the `ItemId::database_id()`,
1309    /// - `id` cannot be found,
1310    /// - `id.index` is out of range for the list of paths under this `name`,
1311    /// - destination `name` already exists at the same relative `path`,
1312    /// - underlying filesystem rename fails.
1313    ///
1314    /// # Examples
1315    /// ```no_run
1316    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1317    ///
1318    /// fn main() -> Result<(), DatabaseError> {
1319    ///     let mut manager = DatabaseManager::new(".", "database")?;
1320    ///     manager.write_new(ItemId::id("old.txt"), ItemId::database_id())?;
1321    ///     manager.rename(ItemId::id("old.txt"), "new.txt")?;
1322    ///     Ok(())
1323    /// }
1324    /// ```
1325    pub fn rename(
1326        &mut self,
1327        id: impl Into<ItemId>,
1328        to: impl AsRef<str>,
1329    ) -> Result<(), DatabaseError> {
1330        let id = id.into();
1331        let name = to.as_ref().to_owned();
1332
1333        if id.get_name().is_empty() {
1334            return Err(DatabaseError::RootIdUnsupported);
1335        }
1336
1337        let path = self.locate_absolute(&id)?;
1338        let mut relative_path = self.locate_relative(&id)?.to_path_buf();
1339
1340        let renamed_path = path.with_file_name(&name);
1341        relative_path = match relative_path.pop() {
1342            true => {
1343                relative_path.push(&name);
1344                relative_path
1345            }
1346            false => PathBuf::from(&name),
1347        };
1348
1349        if self
1350            .items
1351            .get(&name)
1352            .is_some_and(|paths| paths.iter().any(|entry| entry == &relative_path))
1353        {
1354            return Err(DatabaseError::IdAlreadyExists(name));
1355        }
1356
1357        fs::rename(&path, renamed_path)?;
1358
1359        let old_name = id.get_name().to_string();
1360        let old_paths = self
1361            .items
1362            .get_mut(&old_name)
1363            .ok_or_else(|| DatabaseError::NoMatchingID(id.as_string()))?;
1364
1365        if id.get_index() >= old_paths.len() {
1366            return Err(DatabaseError::IndexOutOfBounds {
1367                id: id.as_string(),
1368                index: id.get_index(),
1369                len: old_paths.len(),
1370            });
1371        }
1372
1373        old_paths.remove(id.get_index());
1374        if old_paths.is_empty() {
1375            self.items.remove(&old_name);
1376        }
1377
1378        self.items.entry(name).or_default().push(relative_path);
1379
1380        Ok(())
1381    }
1382
1383    /// Deletes a file, directory, or the whole database root.
1384    ///
1385    /// # Parameters
1386    /// - `id`: item to delete. Use `ItemId::database_id()` to target the database folder itself.
1387    /// - `force`: when deleting directories, controls recursive vs empty-only behavior.
1388    ///
1389    /// # Errors
1390    /// Returns an error if:
1391    /// - `id` cannot be found,
1392    /// - `id.index` is out of range for the list of paths under this `name`,
1393    /// - directory deletion does not match `force` rules,
1394    /// - filesystem delete operations fail.
1395    ///
1396    /// # Examples
1397    /// ```no_run
1398    /// use file_database::{DatabaseError, DatabaseManager, ForceDeletion, ItemId};
1399    ///
1400    /// fn main() -> Result<(), DatabaseError> {
1401    ///     let mut manager = DatabaseManager::new(".", "database")?;
1402    ///     manager.write_new(ItemId::id("tmp.txt"), ItemId::database_id())?;
1403    ///     manager.delete(ItemId::id("tmp.txt"), ForceDeletion::Force)?;
1404    ///     Ok(())
1405    /// }
1406    /// ```
1407    pub fn delete(
1408        &mut self,
1409        id: impl Into<ItemId>,
1410        force: impl Into<bool>,
1411    ) -> Result<(), DatabaseError> {
1412        let id = id.into();
1413
1414        if id.get_name().is_empty() {
1415            match delete_directory(&self.locate_absolute(id)?, force) {
1416                Ok(_) => {
1417                    self.path = PathBuf::new();
1418                    self.items.drain();
1419                    return Ok(());
1420                }
1421                Err(error) => return Err(error),
1422            }
1423        }
1424
1425        let path = self.locate_absolute(&id)?;
1426
1427        if path.is_dir() {
1428            delete_directory(&path, force)?;
1429        } else {
1430            remove_file(path)?;
1431        }
1432
1433        let key = id.get_name().to_string();
1434        let paths = self
1435            .items
1436            .get_mut(&key)
1437            .ok_or_else(|| DatabaseError::NoMatchingID(id.as_string()))?;
1438
1439        if id.get_index() >= paths.len() {
1440            return Err(DatabaseError::IndexOutOfBounds {
1441                id: id.as_string(),
1442                index: id.get_index(),
1443                len: paths.len(),
1444            });
1445        }
1446
1447        paths.remove(id.get_index());
1448        if paths.is_empty() {
1449            self.items.remove(&key);
1450        }
1451
1452        Ok(())
1453    }
1454
1455    /// Gets the absolute file path for an **`ItemId`**.
1456    ///
1457    /// For the `ItemId::database_id()`, this returns the database directory path.
1458    ///
1459    /// # Parameters
1460    /// - `id`: **`ItemId`** to look up.
1461    ///
1462    /// # Errors
1463    /// Returns an error if:
1464    /// - `id.name` does not exist,
1465    /// - `id.index` is out of bounds.
1466    ///
1467    /// # Examples
1468    /// ```no_run
1469    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1470    ///
1471    /// fn main() -> Result<(), DatabaseError> {
1472    ///     let mut manager = DatabaseManager::new(".", "database")?;
1473    ///     manager.write_new(ItemId::id("a.txt"), ItemId::database_id())?;
1474    ///     let _path = manager.locate_absolute(ItemId::id("a.txt"))?;
1475    ///     Ok(())
1476    /// }
1477    /// ```
1478    pub fn locate_absolute(&self, id: impl Into<ItemId>) -> Result<PathBuf, DatabaseError> {
1479        let id = id.into();
1480
1481        if id.get_name().is_empty() {
1482            return Ok(self.path.to_path_buf());
1483        }
1484
1485        Ok(self.path.join(self.resolve_path_by_id(&id)?))
1486    }
1487
1488    /// Gets the stored relative path reference for an **`ItemId`**.
1489    ///
1490    /// For the `ItemId::database_id()`, this currently returns a reference to the manager root path.
1491    ///
1492    /// # Parameters
1493    /// - `id`: **`ItemId`** to look up.
1494    ///
1495    /// # Errors
1496    /// Returns an error if:
1497    /// - `id.name` does not exist,
1498    /// - `id.index` is out of bounds.
1499    ///
1500    /// # Examples
1501    /// ```no_run
1502    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1503    ///
1504    /// fn main() -> Result<(), DatabaseError> {
1505    ///     let mut manager = DatabaseManager::new(".", "database")?;
1506    ///     manager.write_new(ItemId::id("a.txt"), ItemId::database_id())?;
1507    ///     let _relative = manager.locate_relative(ItemId::id("a.txt"))?;
1508    ///     Ok(())
1509    /// }
1510    /// ```
1511    pub fn locate_relative(&self, id: impl Into<ItemId>) -> Result<&PathBuf, DatabaseError> {
1512        let id = id.into();
1513        if id.get_name().is_empty() {
1514            return Ok(&self.path);
1515        }
1516
1517        self.resolve_path_by_id(&id)
1518    }
1519
1520    /// Returns all stored relative paths for a shared `name`.
1521    ///
1522    /// # Parameters
1523    /// - `id`: shared-name **`ItemId`**. `index` is ignored for lookup.
1524    ///
1525    /// # Errors
1526    /// Returns an error if:
1527    /// - `id` is the `ItemId::database_id()`,
1528    /// - no entry exists for `id.name`.
1529    ///
1530    /// # Examples
1531    /// ```no_run
1532    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1533    ///
1534    /// fn main() -> Result<(), DatabaseError> {
1535    ///     let mut manager = DatabaseManager::new(".", "database")?;
1536    ///     manager.write_new(ItemId::id("a.txt"), ItemId::database_id())?;
1537    ///     let _paths = manager.get_paths_for_id(ItemId::id("a.txt"))?;
1538    ///     Ok(())
1539    /// }
1540    /// ```
1541    pub fn get_paths_for_id(&self, id: impl Into<ItemId>) -> Result<&Vec<PathBuf>, DatabaseError> {
1542        let id = id.into();
1543
1544        if id.get_name().is_empty() {
1545            return Err(DatabaseError::RootIdUnsupported);
1546        }
1547
1548        self.items
1549            .get(id.get_name())
1550            .ok_or_else(|| DatabaseError::NoMatchingID(id.as_string()))
1551    }
1552
1553    /// Returns all specific **`ItemId`** values for a shared `name`.
1554    ///
1555    /// # Parameters
1556    /// - `id`: shared-name **`ItemId`**. `index` is ignored for lookup.
1557    ///
1558    /// # Errors
1559    /// Returns an error if:
1560    /// - `ItemId::database_id()` is provided,
1561    /// - no entry exists for `id.name`.
1562    ///
1563    /// # Examples
1564    /// ```no_run
1565    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1566    ///
1567    /// fn main() -> Result<(), DatabaseError> {
1568    ///     let mut manager = DatabaseManager::new(".", "database")?;
1569    ///     manager.write_new(ItemId::id("a.txt"), ItemId::database_id())?;
1570    ///     let _ids = manager.get_ids_from_shared_id(ItemId::id("a.txt"))?;
1571    ///     Ok(())
1572    /// }
1573    /// ```
1574    pub fn get_ids_from_shared_id(
1575        &self,
1576        id: impl Into<ItemId>,
1577    ) -> Result<Vec<ItemId>, DatabaseError> {
1578        let id = id.into();
1579
1580        let paths = self.get_paths_for_id(&id)?;
1581
1582        let ids = paths
1583            .iter()
1584            .enumerate()
1585            .map(|(index, _)| ItemId::with_index(id.get_name().to_string(), index))
1586            .collect();
1587
1588        Ok(ids)
1589    }
1590
1591    /// Scans files on disk and compares them to entries in this scan area.
1592    ///
1593    /// Missing tracked items are always removed from the `items` index kept in memory.
1594    ///
1595    /// Policy behavior for newly discovered external items:
1596    /// - `DetectOnly`: report only.
1597    /// - `AddNew`: report and add to the `index`.
1598    /// - `RemoveNew`: report and delete from disk.
1599    ///
1600    /// # Parameters
1601    /// - `scan_from`: root **`ItemId`** to scan from (`ItemId::database_id()` scans the full database).
1602    /// - `policy`: change handling policy.
1603    /// - `recursive`: `true` scans full subtree, `false` scans immediate children only.
1604    ///
1605    /// # Errors
1606    /// Returns an error if:
1607    /// - `scan_from` cannot be found,
1608    /// - `scan_from` points to a file,
1609    /// - path-to-string conversion fails for discovered entries,
1610    /// - filesystem read or delete operations fail.
1611    ///
1612    /// # Examples
1613    /// ```no_run
1614    /// use file_database::{DatabaseError, DatabaseManager, ItemId, ScanPolicy};
1615    ///
1616    /// fn main() -> Result<(), DatabaseError> {
1617    ///     let mut manager = DatabaseManager::new(".", "database")?;
1618    ///     let _report = manager.scan_for_changes(ItemId::database_id(), ScanPolicy::AddNew, true)?;
1619    ///     Ok(())
1620    /// }
1621    /// ```
1622    pub fn scan_for_changes(
1623        &mut self,
1624        scan_from: impl Into<ItemId>,
1625        policy: ScanPolicy,
1626        recursive: bool,
1627    ) -> Result<ScanReport, DatabaseError> {
1628        let scan_from = scan_from.into();
1629        let scan_from_absolute = self.locate_absolute(&scan_from)?;
1630        if !scan_from_absolute.is_dir() {
1631            return Err(DatabaseError::NotADirectory(scan_from_absolute));
1632        }
1633
1634        let scope_relative = if scan_from.get_name().is_empty() {
1635            None
1636        } else {
1637            Some(self.locate_relative(&scan_from)?.clone())
1638        };
1639
1640        let discovered_paths = self.collect_paths_in_scope(&scan_from_absolute, recursive)?;
1641        let discovered_set: HashSet<PathBuf> = discovered_paths.iter().cloned().collect();
1642
1643        let mut existing_in_scope_set = HashSet::new();
1644        let mut removed = Vec::new();
1645        let mut unchanged_count = 0usize;
1646
1647        for (name, paths) in &self.items {
1648            for (index, path) in paths.iter().enumerate() {
1649                if !is_path_in_scope(path, scope_relative.as_deref(), recursive) {
1650                    continue;
1651                }
1652
1653                existing_in_scope_set.insert(path.clone());
1654
1655                if discovered_set.contains(path) {
1656                    unchanged_count += 1;
1657                } else {
1658                    removed.push(ExternalChange::Removed {
1659                        id: ItemId::with_index(name.clone(), index),
1660                        path: path.clone(),
1661                    });
1662                }
1663            }
1664        }
1665
1666        let mut added_paths: Vec<PathBuf> = discovered_paths
1667            .into_iter()
1668            .filter(|path| !existing_in_scope_set.contains(path))
1669            .collect();
1670
1671        let mut added = Vec::new();
1672        let mut add_offsets: HashMap<String, usize> = HashMap::new();
1673        for path in &added_paths {
1674            let name = path
1675                .file_name()
1676                .and_then(|name| name.to_str())
1677                .ok_or(DatabaseError::OsStringConversion)?
1678                .to_string();
1679            let base_len = self.items.get(&name).map(|paths| paths.len()).unwrap_or(0);
1680            let offset = add_offsets.entry(name.clone()).or_insert(0);
1681            let index = base_len + *offset;
1682            *offset += 1;
1683
1684            added.push(ExternalChange::Added {
1685                id: ItemId::with_index(name, index),
1686                path: path.clone(),
1687            });
1688        }
1689
1690        let mut empty_keys = Vec::new();
1691        for (name, paths) in self.items.iter_mut() {
1692            paths.retain(|path| {
1693                !is_path_in_scope(path, scope_relative.as_deref(), recursive)
1694                    || discovered_set.contains(path)
1695            });
1696            if paths.is_empty() {
1697                empty_keys.push(name.clone());
1698            }
1699        }
1700        for key in empty_keys {
1701            self.items.remove(&key);
1702        }
1703
1704        match policy {
1705            ScanPolicy::DetectOnly => (),
1706            ScanPolicy::AddNew => {
1707                for path in &added_paths {
1708                    let name = path
1709                        .file_name()
1710                        .and_then(|name| name.to_str())
1711                        .ok_or(DatabaseError::OsStringConversion)?
1712                        .to_string();
1713                    self.items.entry(name).or_default().push(path.clone());
1714                }
1715            }
1716            ScanPolicy::RemoveNew => {
1717                added_paths.sort_by_key(|path| std::cmp::Reverse(path.components().count()));
1718                for path in added_paths {
1719                    let absolute = self.path.join(&path);
1720                    if !absolute.exists() {
1721                        continue;
1722                    }
1723
1724                    if absolute.is_dir() {
1725                        remove_dir_all(&absolute)?;
1726                    } else if absolute.is_file() {
1727                        remove_file(&absolute)?;
1728                    }
1729                }
1730            }
1731        }
1732
1733        let total_changed_count = added.len() + removed.len();
1734
1735        Ok(ScanReport {
1736            scanned_from: scan_from,
1737            recursive,
1738            added,
1739            removed,
1740            unchanged_count,
1741            total_changed_count,
1742        })
1743    }
1744
1745    /// Moves the entire database directory to a new parent directory.
1746    ///
1747    /// Existing destination database directory with the same name is removed first.
1748    ///
1749    /// # Parameters
1750    /// - `to`: destination parent directory.
1751    ///
1752    /// # Errors
1753    /// Returns an error if:
1754    /// - current database path is invalid,
1755    /// - destination cleanup fails,
1756    /// - recursive copy or source removal fails.
1757    ///
1758    /// # Examples
1759    /// ```no_run
1760    /// use file_database::{DatabaseError, DatabaseManager};
1761    ///
1762    /// fn main() -> Result<(), DatabaseError> {
1763    ///     let mut manager = DatabaseManager::new(".", "database")?;
1764    ///     manager.migrate_database("./new_parent")?;
1765    ///     Ok(())
1766    /// }
1767    /// ```
1768    pub fn migrate_database(&mut self, to: impl AsRef<Path>) -> Result<(), DatabaseError> {
1769        let destination = to.as_ref().to_path_buf();
1770        let name = self
1771            .path
1772            .file_name()
1773            .ok_or_else(|| DatabaseError::NotADirectory(self.path.clone()))?;
1774        let destination_database_path = destination.join(name);
1775
1776        if destination_database_path.exists() {
1777            remove_dir_all(&destination_database_path)?;
1778        }
1779
1780        copy_directory_recursive(&self.path, &destination_database_path)?;
1781        remove_dir_all(&self.path)?;
1782
1783        self.path = destination_database_path;
1784
1785        Ok(())
1786    }
1787
1788    /// Moves a managed item to another directory inside the same database.
1789    ///
1790    /// # Parameters
1791    /// - `id`: source item to move.
1792    /// - `to`: destination directory item (or `ItemId::database_id()`).
1793    ///
1794    /// # Errors
1795    /// Returns an error if:
1796    /// - `id` is root or cannot be found,
1797    /// - destination is not a directory,
1798    /// - source and destination are identical,
1799    /// - `id.index` is out of bounds for the source `name` vector,
1800    /// - filesystem move fails.
1801    ///
1802    /// # Examples
1803    /// ```no_run
1804    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
1805    ///
1806    /// fn main() -> Result<(), DatabaseError> {
1807    ///     let mut manager = DatabaseManager::new(".", "database")?;
1808    ///     manager.write_new(ItemId::id("folder"), ItemId::database_id())?;
1809    ///     manager.write_new(ItemId::id("a.txt"), ItemId::database_id())?;
1810    ///     manager.migrate_item(ItemId::id("a.txt"), ItemId::id("folder"))?;
1811    ///     Ok(())
1812    /// }
1813    /// ```
1814    pub fn migrate_item(
1815        &mut self,
1816        id: impl Into<ItemId>,
1817        to: impl Into<ItemId>,
1818    ) -> Result<(), DatabaseError> {
1819        let id = id.into();
1820        let to = to.into();
1821
1822        if id.get_name().is_empty() {
1823            return Err(DatabaseError::RootIdUnsupported);
1824        }
1825
1826        let destination_dir = self.locate_absolute(&to)?;
1827        if !destination_dir.is_dir() {
1828            return Err(DatabaseError::NotADirectory(destination_dir));
1829        }
1830
1831        let source_absolute = self.locate_absolute(&id)?;
1832        let source_name = source_absolute
1833            .file_name()
1834            .ok_or_else(|| DatabaseError::NoMatchingID(id.as_string()))?;
1835        let destination_absolute = destination_dir.join(source_name);
1836
1837        if destination_absolute == source_absolute {
1838            return Err(DatabaseError::IdenticalSourceDestination(
1839                destination_absolute,
1840            ));
1841        }
1842
1843        if destination_absolute.exists() {
1844            if destination_absolute.is_dir() {
1845                remove_dir_all(&destination_absolute)?;
1846            } else {
1847                remove_file(&destination_absolute)?;
1848            }
1849        }
1850
1851        fs::rename(&source_absolute, &destination_absolute)?;
1852
1853        let old_name = id.get_name().to_string();
1854        let old_paths = self
1855            .items
1856            .get_mut(&old_name)
1857            .ok_or_else(|| DatabaseError::NoMatchingID(id.as_string()))?;
1858
1859        if id.get_index() >= old_paths.len() {
1860            return Err(DatabaseError::IndexOutOfBounds {
1861                id: id.as_string(),
1862                index: id.get_index(),
1863                len: old_paths.len(),
1864            });
1865        }
1866
1867        old_paths.remove(id.get_index());
1868        if old_paths.is_empty() {
1869            self.items.remove(&old_name);
1870        }
1871
1872        let relative_destination = destination_absolute.strip_prefix(&self.path)?.to_path_buf();
1873        let new_name = match relative_destination.file_name() {
1874            Some(name) => os_str_to_string(Some(name))?,
1875            None => old_name,
1876        };
1877
1878        self.items
1879            .entry(new_name)
1880            .or_default()
1881            .push(relative_destination);
1882
1883        Ok(())
1884    }
1885
1886    /// Exports a managed file or directory to an external destination directory.
1887    ///
1888    /// `Copy` keeps the item in the `index`. `Move` removes the moved entry from the `index`.
1889    ///
1890    /// # Parameters
1891    /// - `id`: source item to export.
1892    /// - `to`: external destination directory path.
1893    /// - `mode`: copy or move behavior.
1894    ///
1895    /// # Errors
1896    /// Returns an error if:
1897    /// - `id` is root or cannot be found,
1898    /// - destination is inside the database,
1899    /// - destination path cannot be created or used as a directory,
1900    /// - `id.index` is out of bounds when removing moved entries,
1901    /// - filesystem copy/move operations fail.
1902    ///
1903    /// # Examples
1904    /// ```no_run
1905    /// use file_database::{DatabaseError, DatabaseManager, ExportMode, ItemId};
1906    ///
1907    /// fn main() -> Result<(), DatabaseError> {
1908    ///     let mut manager = DatabaseManager::new(".", "database")?;
1909    ///     manager.write_new(ItemId::id("a.txt"), ItemId::database_id())?;
1910    ///     manager.export_item(ItemId::id("a.txt"), "./exports", ExportMode::Copy)?;
1911    ///     Ok(())
1912    /// }
1913    /// ```
1914    pub fn export_item(
1915        &mut self,
1916        id: impl Into<ItemId>,
1917        to: impl AsRef<Path>,
1918        mode: ExportMode,
1919    ) -> Result<(), DatabaseError> {
1920        let id = id.into();
1921        let destination_dir = {
1922            let to = to.as_ref();
1923            if to.is_absolute() {
1924                to.to_path_buf()
1925            } else {
1926                current_dir()?.join(to)
1927            }
1928        };
1929
1930        if id.get_name().is_empty() {
1931            return Err(DatabaseError::RootIdUnsupported);
1932        }
1933
1934        if destination_dir.starts_with(&self.path) {
1935            return Err(DatabaseError::ExportDestinationInsideDatabase(
1936                destination_dir,
1937            ));
1938        }
1939
1940        fs::create_dir_all(&destination_dir)?;
1941        if !destination_dir.is_dir() {
1942            return Err(DatabaseError::NotADirectory(destination_dir));
1943        }
1944
1945        let source_absolute = self.locate_absolute(&id)?;
1946        let source_name = source_absolute
1947            .file_name()
1948            .ok_or_else(|| DatabaseError::NoMatchingID(id.as_string()))?;
1949        let destination_absolute = destination_dir.join(source_name);
1950
1951        if destination_absolute == source_absolute {
1952            return Err(DatabaseError::IdenticalSourceDestination(
1953                destination_absolute,
1954            ));
1955        }
1956
1957        if destination_absolute.exists() {
1958            if destination_absolute.is_dir() {
1959                remove_dir_all(&destination_absolute)?;
1960            } else {
1961                remove_file(&destination_absolute)?;
1962            }
1963        }
1964
1965        match mode {
1966            ExportMode::Copy => {
1967                if source_absolute.is_dir() {
1968                    copy_directory_recursive(&source_absolute, &destination_absolute)?;
1969                } else {
1970                    fs::copy(&source_absolute, &destination_absolute)?;
1971                }
1972            }
1973            ExportMode::Move => {
1974                match fs::rename(&source_absolute, &destination_absolute) {
1975                    Ok(_) => (),
1976                    Err(_) => {
1977                        if source_absolute.is_dir() {
1978                            copy_directory_recursive(&source_absolute, &destination_absolute)?;
1979                            remove_dir_all(&source_absolute)?;
1980                        } else {
1981                            fs::copy(&source_absolute, &destination_absolute)?;
1982                            remove_file(&source_absolute)?;
1983                        }
1984                    }
1985                }
1986
1987                let key = id.get_name().to_string();
1988                let paths = self
1989                    .items
1990                    .get_mut(&key)
1991                    .ok_or_else(|| DatabaseError::NoMatchingID(id.as_string()))?;
1992
1993                if id.get_index() >= paths.len() {
1994                    return Err(DatabaseError::IndexOutOfBounds {
1995                        id: id.as_string(),
1996                        index: id.get_index(),
1997                        len: paths.len(),
1998                    });
1999                }
2000
2001                paths.remove(id.get_index());
2002                if paths.is_empty() {
2003                    self.items.remove(&key);
2004                }
2005            }
2006        }
2007
2008        Ok(())
2009    }
2010
2011    /// Imports an external file or directory into a database destination directory.
2012    ///
2013    /// The imported item keeps its original `name`.
2014    ///
2015    /// # Parameters
2016    /// - `from`: source path outside the database.
2017    /// - `to`: destination directory item in the database.
2018    ///
2019    /// # Errors
2020    /// Returns an error if:
2021    /// - source path points inside the database,
2022    /// - destination is not a directory,
2023    /// - destination `path`/`name` already exists,
2024    /// - source does not exist as file or directory,
2025    /// - filesystem copy operations fail.
2026    ///
2027    /// # Examples
2028    /// ```no_run
2029    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
2030    ///
2031    /// fn main() -> Result<(), DatabaseError> {
2032    ///     let mut manager = DatabaseManager::new(".", "database")?;
2033    ///     manager.write_new(ItemId::id("imports"), ItemId::database_id())?;
2034    ///     manager.import_item("./outside/example.txt", ItemId::id("imports"))?;
2035    ///     Ok(())
2036    /// }
2037    /// ```
2038    pub fn import_item(
2039        &mut self,
2040        from: impl AsRef<Path>,
2041        to: impl Into<ItemId>,
2042    ) -> Result<(), DatabaseError> {
2043        let source_path = {
2044            let from = from.as_ref();
2045            if from.is_absolute() {
2046                from.to_path_buf()
2047            } else {
2048                current_dir()?.join(from)
2049            }
2050        };
2051        let to = to.into();
2052
2053        if source_path.starts_with(&self.path) {
2054            return Err(DatabaseError::ImportSourceInsideDatabase(source_path));
2055        }
2056
2057        let destination_parent = self.locate_absolute(&to)?;
2058        if !destination_parent.is_dir() {
2059            return Err(DatabaseError::NotADirectory(destination_parent));
2060        }
2061
2062        let item_name = source_path
2063            .file_name()
2064            .ok_or_else(|| DatabaseError::NotAFile(source_path.clone()))?
2065            .to_string_lossy()
2066            .to_string();
2067
2068        let destination_absolute = destination_parent.join(&item_name);
2069        let destination_relative = if to.get_name().is_empty() {
2070            PathBuf::from(&item_name)
2071        } else {
2072            let mut relative = self.locate_relative(&to)?.to_path_buf();
2073            relative.push(&item_name);
2074            relative
2075        };
2076
2077        if destination_absolute.exists()
2078            || self
2079                .items
2080                .get(&item_name)
2081                .is_some_and(|paths| paths.iter().any(|path| path == &destination_relative))
2082        {
2083            return Err(DatabaseError::IdAlreadyExists(item_name));
2084        }
2085
2086        if source_path.is_dir() {
2087            copy_directory_recursive(&source_path, &destination_absolute)?;
2088        } else if source_path.is_file() {
2089            fs::copy(&source_path, &destination_absolute)?;
2090        } else {
2091            return Err(DatabaseError::NoMatchingID(
2092                source_path.display().to_string(),
2093            ));
2094        }
2095
2096        self.items
2097            .entry(item_name)
2098            .or_default()
2099            .push(destination_relative);
2100
2101        Ok(())
2102    }
2103
2104    /// Duplicates a managed item into `parent` using a caller-provided `name`.
2105    ///
2106    /// # Parameters
2107    /// - `id`: source item to duplicate.
2108    /// - `parent`: destination parent directory item (or `ItemId::database_id()`).
2109    /// - `name`: new name for the duplicate.
2110    ///
2111    /// # Errors
2112    /// Returns an error if:
2113    /// - `id` is root or cannot be found,
2114    /// - destination parent is not a directory,
2115    /// - destination `name` already exists in the target directory,
2116    /// - filesystem copy fails.
2117    ///
2118    /// # Examples
2119    /// ```no_run
2120    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
2121    ///
2122    /// fn main() -> Result<(), DatabaseError> {
2123    ///     let mut manager = DatabaseManager::new(".", "database")?;
2124    ///     manager.write_new(ItemId::id("a.txt"), ItemId::database_id())?;
2125    ///     manager.duplicate_item(ItemId::id("a.txt"), ItemId::database_id(), "copy.txt")?;
2126    ///     Ok(())
2127    /// }
2128    /// ```
2129    pub fn duplicate_item(
2130        &mut self,
2131        id: impl Into<ItemId>,
2132        parent: impl Into<ItemId>,
2133        name: impl AsRef<str>,
2134    ) -> Result<(), DatabaseError> {
2135        let id = id.into();
2136        let parent = parent.into();
2137        let name = name.as_ref().to_owned();
2138
2139        if id.get_name().is_empty() {
2140            return Err(DatabaseError::RootIdUnsupported);
2141        }
2142
2143        let source_absolute = self.locate_absolute(&id)?;
2144        let parent_absolute = self.locate_absolute(&parent)?;
2145        if !parent_absolute.is_dir() {
2146            return Err(DatabaseError::NotADirectory(parent_absolute));
2147        }
2148
2149        let destination_absolute = parent_absolute.join(&name);
2150        let destination_relative = if parent.get_name().is_empty() {
2151            PathBuf::from(&name)
2152        } else {
2153            let mut path = self.locate_relative(&parent)?.to_path_buf();
2154            path.push(&name);
2155            path
2156        };
2157
2158        if destination_absolute.exists()
2159            || self
2160                .items
2161                .get(&name)
2162                .is_some_and(|paths| paths.iter().any(|path| path == &destination_relative))
2163        {
2164            return Err(DatabaseError::IdAlreadyExists(name));
2165        }
2166
2167        if source_absolute.is_dir() {
2168            copy_directory_recursive(&source_absolute, &destination_absolute)?;
2169        } else {
2170            fs::copy(&source_absolute, &destination_absolute)?;
2171        }
2172
2173        self.items
2174            .entry(
2175                destination_relative
2176                    .file_name()
2177                    .map(|name| name.to_string_lossy().to_string())
2178                    .unwrap_or_default(),
2179            )
2180            .or_default()
2181            .push(destination_relative);
2182
2183        Ok(())
2184    }
2185
2186    /// Returns filesystem metadata summary for a managed file or directory.
2187    ///
2188    /// Includes:
2189    /// - `name`/`extension`,
2190    /// - normalized size,
2191    /// - Unix timestamps and "time since" timestamps where available.
2192    ///
2193    /// # Parameters
2194    /// - `id`: item to inspect.
2195    ///
2196    /// # Errors
2197    /// Returns an error if:
2198    /// - `id` cannot be found,
2199    /// - metadata lookup fails.
2200    ///
2201    /// # Examples
2202    /// ```no_run
2203    /// use file_database::{DatabaseError, DatabaseManager, ItemId};
2204    ///
2205    /// fn main() -> Result<(), DatabaseError> {
2206    ///     let mut manager = DatabaseManager::new(".", "database")?;
2207    ///     manager.write_new(ItemId::id("a.txt"), ItemId::database_id())?;
2208    ///     let _info = manager.get_file_information(ItemId::id("a.txt"))?;
2209    ///     Ok(())
2210    /// }
2211    /// ```
2212    pub fn get_file_information(
2213        &self,
2214        id: impl Into<ItemId>,
2215    ) -> Result<FileInformation, DatabaseError> {
2216        let id = id.into();
2217
2218        let path = self.locate_absolute(id)?;
2219
2220        let metadata = fs::metadata(&path)?;
2221
2222        let name = {
2223            let os = if path.is_dir() {
2224                path.file_name()
2225            } else {
2226                path.file_stem()
2227            };
2228
2229            match os_str_to_string(os) {
2230                Ok(name) => Some(name),
2231                Err(_) => None,
2232            }
2233        };
2234
2235        let extension = {
2236            if path.is_dir() {
2237                None
2238            } else {
2239                match os_str_to_string(path.extension()) {
2240                    Ok(extension) => Some(extension),
2241                    Err(_) => None,
2242                }
2243            }
2244        };
2245
2246        let size = FileSize::from(metadata.len());
2247
2248        let unix_created = sys_time_to_unsigned_int(metadata.created());
2249        let time_since_created = sys_time_to_time_since(metadata.created());
2250
2251        let unix_last_opened = sys_time_to_unsigned_int(metadata.accessed());
2252        let time_since_last_opened = sys_time_to_time_since(metadata.accessed());
2253
2254        let unix_last_modified = sys_time_to_unsigned_int(metadata.modified());
2255        let time_since_last_modified = sys_time_to_time_since(metadata.modified());
2256
2257        Ok(FileInformation {
2258            name,
2259            extension,
2260            size,
2261            unix_created,
2262            time_since_created,
2263            unix_last_opened,
2264            time_since_last_opened,
2265            unix_last_modified,
2266            time_since_last_modified,
2267        })
2268    }
2269
2270    /// Gets one specific path from a shared `name` + `index`.
2271    ///
2272    /// # Errors
2273    /// Returns an error if:
2274    /// - the shared `name` key does not exist,
2275    /// - `id.index` is out of bounds.
2276    fn resolve_path_by_id(&self, id: &ItemId) -> Result<&PathBuf, DatabaseError> {
2277        let matches = self
2278            .items
2279            .get(id.get_name())
2280            .ok_or_else(|| DatabaseError::NoMatchingID(id.as_string()))?;
2281
2282        if id.get_index() >= matches.len() {
2283            return Err(DatabaseError::IndexOutOfBounds {
2284                id: id.as_string(),
2285                index: id.get_index(),
2286                len: matches.len(),
2287            });
2288        }
2289
2290        Ok(&matches[id.get_index()])
2291    }
2292
2293    /// Overwrites a file safely by using a temp file and rename.
2294    ///
2295    /// `write_fn` is responsible for writing bytes to the temporary file and returning
2296    /// the number of bytes written.
2297    ///
2298    /// # Errors
2299    /// Returns an error if:
2300    /// - `path` points to a directory,
2301    /// - temp create/write/sync/rename fails.
2302    fn overwrite_path_atomic_with<F>(&self, path: &Path, write_fn: F) -> Result<u64, DatabaseError>
2303    where
2304        F: FnOnce(&mut File) -> Result<u64, DatabaseError>,
2305    {
2306        if path.is_dir() {
2307            return Err(DatabaseError::NotAFile(path.to_path_buf()));
2308        }
2309
2310        let buffer = path.with_extension("tmp");
2311
2312        let result = (|| {
2313            let mut file = File::create(&buffer)?;
2314            let bytes_written = write_fn(&mut file)?;
2315            file.sync_all()?;
2316            fs::rename(&buffer, path)?;
2317            Ok(bytes_written)
2318        })();
2319
2320        if result.is_err() && buffer.exists() {
2321            let _ = remove_file(&buffer);
2322        }
2323
2324        result
2325    }
2326
2327    /// Collects relative file and folder paths in the scan area.
2328    ///
2329    /// # Parameters
2330    /// - `scope_absolute`: absolute root directory for collection.
2331    /// - `recursive`: whether to include descendants recursively.
2332    ///
2333    /// # Errors
2334    /// Returns an error if reading folders fails or converting to a relative prefix fails.
2335    fn collect_paths_in_scope(
2336        &self,
2337        scope_absolute: &Path,
2338        recursive: bool,
2339    ) -> Result<Vec<PathBuf>, DatabaseError> {
2340        let mut collected = Vec::new();
2341
2342        if recursive {
2343            let mut stack = vec![scope_absolute.to_path_buf()];
2344            while let Some(directory) = stack.pop() {
2345                for entry in fs::read_dir(&directory)? {
2346                    let entry = entry?;
2347                    let absolute_path = entry.path();
2348                    let relative_path = absolute_path.strip_prefix(&self.path)?.to_path_buf();
2349
2350                    if absolute_path.is_dir() {
2351                        collected.push(relative_path);
2352                        stack.push(absolute_path);
2353                    } else if absolute_path.is_file() {
2354                        collected.push(relative_path);
2355                    }
2356                }
2357            }
2358        } else {
2359            for entry in fs::read_dir(scope_absolute)? {
2360                let entry = entry?;
2361                let absolute_path = entry.path();
2362                let relative_path = absolute_path.strip_prefix(&self.path)?.to_path_buf();
2363
2364                if absolute_path.is_dir() || absolute_path.is_file() {
2365                    collected.push(relative_path);
2366                }
2367            }
2368        }
2369
2370        Ok(collected)
2371    }
2372}
2373
2374// -------- Functions --------
2375/// Removes `steps` trailing segments from `path`.
2376///
2377/// # Errors
2378/// Returns [`DatabaseError::PathStepOverflow`] when `steps` is too large for `path`.
2379fn truncate(mut path: PathBuf, steps: i32) -> Result<PathBuf, DatabaseError> {
2380    let parents = (path.ancestors().count() - 1) as i32;
2381
2382    if parents <= steps {
2383        return Err(DatabaseError::PathStepOverflow(steps, parents));
2384    }
2385
2386    for _ in 0..steps {
2387        path.pop();
2388    }
2389
2390    Ok(path)
2391}
2392
2393/// Converts an optional `OsStr` into an owned `String`.
2394///
2395/// # Errors
2396/// Returns [`DatabaseError::OsStringConversion`] if the value is `None` or invalid UTF-8.
2397fn os_str_to_string(os_str: Option<&OsStr>) -> Result<String, DatabaseError> {
2398    let os_str = match os_str {
2399        Some(os_str) => os_str,
2400        None => return Err(DatabaseError::OsStringConversion),
2401    };
2402
2403    match os_str.to_os_string().into_string() {
2404        Ok(string) => Ok(string),
2405        Err(_) => Err(DatabaseError::OsStringConversion),
2406    }
2407}
2408
2409/// Converts `SystemTime` to Unix timestamp seconds.
2410///
2411/// Returns `None` for platform or conversion failures.
2412fn sys_time_to_unsigned_int(time: io::Result<SystemTime>) -> Option<u64> {
2413    match time {
2414        Ok(time) => match time.duration_since(UNIX_EPOCH) {
2415            Ok(duration) => Some(duration.as_secs()),
2416            Err(_) => None,
2417        },
2418        Err(_) => None,
2419    }
2420}
2421
2422/// Converts `SystemTime` to "time since now" represented as Unix-seconds duration.
2423///
2424/// Returns `None` for platform or conversion failures.
2425fn sys_time_to_time_since(time: io::Result<SystemTime>) -> Option<u64> {
2426    let duration = match time {
2427        Ok(time) => match SystemTime::now().duration_since(time) {
2428            Ok(duration) => duration,
2429            Err(_) => return None,
2430        },
2431        Err(_) => return None,
2432    };
2433
2434    sys_time_to_unsigned_int(Ok(UNIX_EPOCH + duration))
2435}
2436
2437/// Recursively copies a directory tree from `from` to `to`.
2438///
2439/// # Errors
2440/// Returns **`DatabaseError`** if reading folders or copying files fails.
2441fn copy_directory_recursive(from: &Path, to: &Path) -> Result<(), DatabaseError> {
2442    fs::create_dir_all(to)?;
2443
2444    for entry in fs::read_dir(from)? {
2445        let entry = entry?;
2446        let source_path = entry.path();
2447        let destination_path = to.join(entry.file_name());
2448
2449        if source_path.is_dir() {
2450            copy_directory_recursive(&source_path, &destination_path)?;
2451        } else {
2452            fs::copy(&source_path, &destination_path)?;
2453        }
2454    }
2455
2456    Ok(())
2457}
2458
2459/// Returns whether `path` is inside the requested scan scope.
2460fn is_path_in_scope(path: &Path, scope_relative: Option<&Path>, recursive: bool) -> bool {
2461    match scope_relative {
2462        None => {
2463            if recursive {
2464                true
2465            } else {
2466                path.parent()
2467                    .is_some_and(|parent| parent.as_os_str().is_empty())
2468            }
2469        }
2470        Some(scope_relative) => {
2471            if recursive {
2472                path.starts_with(scope_relative) && path != scope_relative
2473            } else {
2474                path.parent() == Some(scope_relative)
2475            }
2476        }
2477    }
2478}
2479
2480/// Deletes a directory `path` in forced or non-forced mode.
2481///
2482/// # Errors
2483/// Returns **`DatabaseError`** if the remove operation fails.
2484fn delete_directory<T>(path: &PathBuf, force: T) -> Result<(), DatabaseError>
2485where
2486    T: Into<bool>,
2487{
2488    if force.into() {
2489        return Ok(remove_dir_all(path)?);
2490    } else {
2491        return Ok(remove_dir(path)?);
2492    }
2493}