database_migration_files/
lib.rs

1#![doc(html_root_url = "https://docs.rs/database-migration-files/0.2.0")]
2
3use database_migration::checksum::hash_migration_script;
4use database_migration::definition::{ExcludedFiles, GetFilename, ParseMigration};
5use database_migration::error::Error;
6use database_migration::migration::{Migration, NewMigration, ScriptContent};
7use database_migration::repository::{CreateNewMigration, ListMigrations, ReadScriptContent};
8use std::fs;
9use std::fs::File;
10#[cfg(target_family = "windows")]
11use std::os::windows::fs::FileTypeExt;
12use std::path::Path;
13use walkdir::WalkDir;
14
15#[derive(Clone)]
16pub struct MigrationDirectory<'a> {
17    path: &'a Path,
18    excluded_files: &'a ExcludedFiles,
19}
20
21impl<'a> MigrationDirectory<'a> {
22    pub const fn new(path: &'a Path, excluded_files: &'a ExcludedFiles) -> Self {
23        Self {
24            path,
25            excluded_files,
26        }
27    }
28
29    pub fn create_directory_if_not_existing(&self) -> Result<(), Error> {
30        if self.path.exists() {
31            return Ok(());
32        }
33        fs::create_dir_all(self.path)
34            .map_err(|err| Error::CreatingMigrationsFolder(err.to_string()))
35    }
36
37    pub const fn files<S>(&self, filename_strategy: S) -> MigrationFiles<'a, S> {
38        MigrationFiles::new(self.path, filename_strategy)
39    }
40}
41
42impl ListMigrations for MigrationDirectory<'_> {
43    type Iter = MigDirIter;
44
45    fn list_all_migrations(&self) -> Result<Self::Iter, Error> {
46        if !self.path.exists() {
47            return Err(Error::ScanningMigrationDirectory(format!(
48                r#"migrations folder "{}" does not exist"#,
49                self.path.display()
50            )));
51        }
52        let walk_dir = WalkDir::new(self.path);
53        Ok(MigDirIter {
54            walker: walk_dir.into_iter(),
55            excluded_files: self.excluded_files.clone(),
56        })
57    }
58}
59
60impl ReadScriptContent for MigrationDirectory<'_> {
61    fn read_script_content(&self, migration: &Migration) -> Result<ScriptContent, Error> {
62        let content = fs::read_to_string(&migration.script_path)
63            .map_err(|err| Error::ReadingMigrationFile(err.to_string()))?;
64        let checksum = hash_migration_script(migration, &content);
65        Ok(ScriptContent {
66            key: migration.key,
67            kind: migration.kind,
68            path: migration.script_path.clone(),
69            content,
70            checksum,
71        })
72    }
73}
74
75#[derive(Debug)]
76pub struct MigDirIter {
77    walker: walkdir::IntoIter,
78    excluded_files: ExcludedFiles,
79}
80
81impl Iterator for MigDirIter {
82    type Item = Result<Migration, Error>;
83
84    fn next(&mut self) -> Option<Self::Item> {
85        for dir_entry in &mut self.walker {
86            return match dir_entry {
87                Ok(entry) => {
88                    let file_type = entry.file_type();
89                    #[cfg(target_family = "windows")]
90                    if file_type.is_dir() || file_type.is_symlink_dir() {
91                        continue;
92                    }
93                    #[cfg(not(target_family = "windows"))]
94                    if file_type.is_dir() {
95                        continue;
96                    }
97                    let file_path = entry.path();
98                    if self.excluded_files.matches(file_path) {
99                        continue;
100                    }
101                    Some(file_path.parse_migration().map_err(Error::from))
102                },
103                Err(err) => Some(Err(Error::ScanningMigrationDirectory(err.to_string()))),
104            };
105        }
106        None
107    }
108
109    fn size_hint(&self) -> (usize, Option<usize>) {
110        self.walker.size_hint()
111    }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct MigrationFiles<'a, S> {
116    path: &'a Path,
117    filename_strategy: S,
118}
119
120impl<'a, S> MigrationFiles<'a, S> {
121    pub const fn new(path: &'a Path, filename_strategy: S) -> Self {
122        Self {
123            path,
124            filename_strategy,
125        }
126    }
127}
128
129impl<S> CreateNewMigration for MigrationFiles<'_, S>
130where
131    S: GetFilename,
132{
133    fn create_new_migration(&self, new_migration: NewMigration) -> Result<Migration, Error> {
134        let filename = self.filename_strategy.get_filename(&new_migration);
135        let script_path = self.path.join(&filename);
136        File::create_new(&script_path).map_err(|err| Error::CreatingScriptFile(err.to_string()))?;
137        Ok(Migration {
138            key: new_migration.key,
139            title: new_migration.title,
140            kind: new_migration.kind,
141            script_path,
142        })
143    }
144}
145
146#[cfg(test)]
147mod tests;
148
149// workaround for false positive 'unused extern crate' warnings until
150// Rust issue [#95513](https://github.com/rust-lang/rust/issues/95513) is fixed
151#[cfg(test)]
152mod dummy_extern_uses {
153    use version_sync as _;
154}