Skip to main content

dotfiles_actions/create/
action.rs

1// Copyright (c) 2021-2026 Miguel Barreto and others
2//
3// Permission is hereby granted, free of charge, to any person obtaining
4// a copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to
8// permit persons to whom the Software is furnished to do so, subject to
9// the following conditions:
10//
11// The above copyright notice and this permission notice shall be
12// included in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22//! This module contains the [CreateAction] that creates a new directory
23//! when executed
24
25extern crate strict_yaml_rust;
26
27use derivative::Derivative;
28use dotfiles_core::error::DotfilesError;
29use dotfiles_core::path::convert_path_to_absolute;
30use dotfiles_core::path::process_home_dir_in_path;
31use dotfiles_core::settings::{initialize_settings_object, Setting, Settings};
32use dotfiles_core::yaml_util;
33use dotfiles_core::{action::SKIP_IN_CI_SETTING, Action};
34use filesystem::FakeFileSystem;
35use filesystem::FileSystem;
36use filesystem::OsFileSystem;
37
38use getset::Getters;
39use log::info;
40
41use std::io::ErrorKind;
42use std::path::Path;
43use std::path::PathBuf;
44use strict_yaml_rust::StrictYaml;
45
46/// Constant for the name of the [`create_parent_dirs`](CreateAction::create_parent_dirs) Setting
47/// which forces to create all parent directories if necessary.
48pub const CREATE_PARENT_DIRS_SETTING: &str = "create_parent_dirs";
49/// Constant for the name of the [`directory`](CreateAction::directory) argument that contains the
50/// name of the directory to create
51pub const DIR_SETTING: &str = "dir";
52
53/// Default settings for the Create action.
54pub fn default_settings() -> Settings {
55  initialize_settings_object(&[
56    (
57      CREATE_PARENT_DIRS_SETTING.to_owned(),
58      Setting::Boolean(false),
59    ),
60    (SKIP_IN_CI_SETTING.to_owned(), Setting::Boolean(false)),
61  ])
62}
63
64/// [CreateAction] creates a new [directory](CreateAction::directory) when executed
65#[derive(Derivative, Getters)]
66#[derivative(Debug, PartialEq)]
67pub struct CreateAction<F: FileSystem> {
68  /// Skips this action if it is running in a CI environment.
69  skip_in_ci: bool,
70  /// FileSystem to use to create the directory.
71  ///
72  /// Having a filesystem instance here allows us to use fakes/mocks to use
73  /// in unit tests.
74  #[derivative(Debug = "ignore", PartialEq = "ignore")]
75  fs: F,
76  /// Directory to create. Can be absolute or relative.
77  #[get = "pub"]
78  directory: String,
79  /// Force creation of the directory and all its parents if they do not
80  /// exist already.
81  ///
82  /// Setting [`create_parent_dirs`](CreateAction::create_parent_dirs) to `true` is equivalent to
83  /// using the `-p` flag in `mkdir`.
84  #[get = "pub"]
85  create_parent_dirs: bool,
86  /// Current directory that will be used to determine relative file locations if necessary. It
87  /// must match the parent directory of the configuration file that declared this action.
88  #[get = "pub"]
89  current_dir: PathBuf,
90}
91
92/// A native create action that works on the real filesystem.
93pub type NativeCreateAction = CreateAction<OsFileSystem>;
94/// A Fake create action that works on a fake test filesystem.
95pub type FakeCreateAction = CreateAction<FakeFileSystem>;
96
97impl<F: FileSystem> CreateAction<F> {
98  /// Constructs a new instance of CreateAction
99  pub fn new(
100    fs: F,
101    skip_in_ci: bool,
102    directory: String,
103    create_parent_dirs: bool,
104    current_dir: PathBuf,
105  ) -> Result<Self, DotfilesError> {
106    let action = CreateAction {
107      skip_in_ci,
108      fs,
109      directory,
110      create_parent_dirs,
111      current_dir,
112    };
113    log::trace!("Creating new {:?}", action);
114    Ok(action)
115  }
116}
117
118impl<F: FileSystem> Action for CreateAction<F> {
119  /// Creates the [`directory`](CreateAction::directory).
120  ///
121  /// # Errors
122  /// - The parent directory does not exist and
123  ///   [`create_parent_dirs`](CreateAction::create_parent_dirs) is false.
124  /// - There is already a directory, file or symlink with the same name.
125  /// - Permission denied.
126  fn execute(&self) -> Result<(), DotfilesError> {
127    fn create_dir<F: FileSystem>(
128      fs: &'_ F,
129      directory: &str,
130      create_parent_dirs: bool,
131      current_dir: &Path,
132    ) -> Result<(), DotfilesError> {
133      let path = PathBuf::from(directory.to_owned());
134      let path = process_home_dir_in_path(&path);
135      let path = convert_path_to_absolute(&path, Some(current_dir))?;
136      if create_parent_dirs {
137        fs.create_dir_all(path)
138      } else {
139        fs.create_dir(path)
140      }
141      .or_else(|io_error| {
142        if let ErrorKind::AlreadyExists = io_error.kind() {
143          Ok(())
144        } else {
145          Err(DotfilesError::from_io_error(io_error))
146        }
147      })
148    }
149    create_dir(
150      &self.fs,
151      &self.directory,
152      self.create_parent_dirs,
153      &self.current_dir,
154    )
155    .map(|_| {
156      info!("Created directory {}", self.directory);
157    })
158  }
159
160  fn skip_in_ci(&self) -> bool {
161    self.skip_in_ci
162  }
163}
164
165/// Static parsing function to build a CreateAction from YAML and settings context
166pub fn parse_action<F: FileSystem>(
167  fs: F,
168  settings: &Settings,
169  yaml: &StrictYaml,
170  current_dir: &Path,
171) -> Result<CreateAction<F>, DotfilesError> {
172  let defaults = default_settings();
173  CreateAction::new(
174    fs,
175    yaml_util::get_boolean_setting_from_yaml_or_context(
176      SKIP_IN_CI_SETTING,
177      yaml,
178      settings,
179      &defaults,
180    )?,
181    yaml_util::get_string_content_or_keyed_value(yaml, Some(DIR_SETTING))?,
182    yaml_util::get_boolean_setting_from_yaml_or_context(
183      CREATE_PARENT_DIRS_SETTING,
184      yaml,
185      settings,
186      &defaults,
187    )?,
188    current_dir.to_owned(),
189  )
190}