forked from lix-project/lix-installer
CreateOrInsertFile -> CreateOrInsertIntoFile
Also move appending behaviour in here.
This commit is contained in:
parent
286f84c53f
commit
d053e7fe69
|
@ -1,216 +0,0 @@
|
|||
use nix::unistd::{chown, Group, User};
|
||||
|
||||
use crate::action::{Action, ActionDescription, ActionError, StatefulAction};
|
||||
use std::{
|
||||
io::SeekFrom,
|
||||
os::unix::prelude::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tokio::{
|
||||
fs::{remove_file, OpenOptions},
|
||||
io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
|
||||
};
|
||||
use tracing::{span, Span};
|
||||
|
||||
/** Create a file at the given location with the provided `buf`,
|
||||
optionally with an owning user, group, and mode.
|
||||
|
||||
If the file exists, the provided `buf` will be appended.
|
||||
*/
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||
pub struct CreateOrAppendFile {
|
||||
path: PathBuf,
|
||||
user: Option<String>,
|
||||
group: Option<String>,
|
||||
mode: Option<u32>,
|
||||
buf: String,
|
||||
}
|
||||
|
||||
impl CreateOrAppendFile {
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub async fn plan(
|
||||
path: impl AsRef<Path>,
|
||||
user: impl Into<Option<String>>,
|
||||
group: impl Into<Option<String>>,
|
||||
mode: impl Into<Option<u32>>,
|
||||
buf: String,
|
||||
) -> Result<StatefulAction<Self>, ActionError> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
|
||||
Ok(Self {
|
||||
path,
|
||||
user: user.into(),
|
||||
group: group.into(),
|
||||
mode: mode.into(),
|
||||
buf,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[typetag::serde(name = "create_or_append_file")]
|
||||
impl Action for CreateOrAppendFile {
|
||||
fn tracing_synopsis(&self) -> String {
|
||||
format!("Create or append file `{}`", self.path.display())
|
||||
}
|
||||
|
||||
fn tracing_span(&self) -> Span {
|
||||
let span = span!(
|
||||
tracing::Level::DEBUG,
|
||||
"create_or_append_file",
|
||||
path = tracing::field::display(self.path.display()),
|
||||
user = self.user,
|
||||
group = self.group,
|
||||
mode = self
|
||||
.mode
|
||||
.map(|v| tracing::field::display(format!("{:#o}", v))),
|
||||
buf = tracing::field::Empty,
|
||||
);
|
||||
|
||||
if tracing::enabled!(tracing::Level::TRACE) {
|
||||
span.record("buf", &self.buf);
|
||||
}
|
||||
span
|
||||
}
|
||||
|
||||
fn execute_description(&self) -> Vec<ActionDescription> {
|
||||
vec![ActionDescription::new(self.tracing_synopsis(), vec![])]
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn execute(&mut self) -> Result<(), ActionError> {
|
||||
let Self {
|
||||
path,
|
||||
user,
|
||||
group,
|
||||
mode,
|
||||
buf,
|
||||
} = self;
|
||||
|
||||
let existed = match tokio::fs::metadata(&path).await {
|
||||
Ok(_) => true,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
|
||||
Err(e) => return Err(ActionError::GettingMetadata(path.to_owned(), e)),
|
||||
};
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.read(true)
|
||||
// If the file is created, ensure that it has harmless
|
||||
// permissions regardless of whether the mode will be
|
||||
// changed later (if we ever create setuid executables,
|
||||
// they should only become setuid once they are owned by
|
||||
// the appropriate user)
|
||||
.mode(0o644 & mode.unwrap_or(0o644))
|
||||
.open(&path)
|
||||
.await
|
||||
.map_err(|e| ActionError::Open(path.to_owned(), e))?;
|
||||
|
||||
file.seek(SeekFrom::End(0))
|
||||
.await
|
||||
.map_err(|e| ActionError::Seek(path.to_owned(), e))?;
|
||||
|
||||
file.write_all(buf.as_bytes())
|
||||
.await
|
||||
.map_err(|e| ActionError::Write(path.to_owned(), e))?;
|
||||
|
||||
let gid = if let Some(group) = group {
|
||||
Some(
|
||||
Group::from_name(group.as_str())
|
||||
.map_err(|e| ActionError::GroupId(group.clone(), e))?
|
||||
.ok_or(ActionError::NoGroup(group.clone()))?
|
||||
.gid,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let uid = if let Some(user) = user {
|
||||
Some(
|
||||
User::from_name(user.as_str())
|
||||
.map_err(|e| ActionError::UserId(user.clone(), e))?
|
||||
.ok_or(ActionError::NoUser(user.clone()))?
|
||||
.uid,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Change ownership _before_ applying mode, to ensure that if
|
||||
// a file needs to be setuid it will never be setuid for the
|
||||
// wrong user
|
||||
chown(path, uid, gid).map_err(|e| ActionError::Chown(path.clone(), e))?;
|
||||
|
||||
if let Some(mode) = mode {
|
||||
tokio::fs::set_permissions(&path, PermissionsExt::from_mode(*mode))
|
||||
.await
|
||||
.map_err(|e| ActionError::SetPermissions(*mode, path.to_owned(), e))?;
|
||||
} else if !existed {
|
||||
tokio::fs::set_permissions(&path, PermissionsExt::from_mode(0o644))
|
||||
.await
|
||||
.map_err(|e| ActionError::SetPermissions(0o644, path.to_owned(), e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn revert_description(&self) -> Vec<ActionDescription> {
|
||||
let Self {
|
||||
path,
|
||||
user: _,
|
||||
group: _,
|
||||
mode: _,
|
||||
buf,
|
||||
} = &self;
|
||||
vec![ActionDescription::new(
|
||||
format!("Delete Nix related fragment from file `{}`", path.display()),
|
||||
vec![format!(
|
||||
"Delete Nix related fragment from file `{}`. Fragment: `{buf}`",
|
||||
path.display()
|
||||
)],
|
||||
)]
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn revert(&mut self) -> Result<(), ActionError> {
|
||||
let Self {
|
||||
path,
|
||||
user: _,
|
||||
group: _,
|
||||
mode: _,
|
||||
buf,
|
||||
} = self;
|
||||
let mut file = OpenOptions::new()
|
||||
.create(false)
|
||||
.write(true)
|
||||
.read(true)
|
||||
.open(&path)
|
||||
.await
|
||||
.map_err(|e| ActionError::Read(path.to_owned(), e))?;
|
||||
|
||||
let mut file_contents = String::default();
|
||||
file.read_to_string(&mut file_contents)
|
||||
.await
|
||||
.map_err(|e| ActionError::Seek(path.to_owned(), e))?;
|
||||
|
||||
if let Some(start) = file_contents.rfind(buf.as_str()) {
|
||||
let end = start + buf.len();
|
||||
file_contents.replace_range(start..end, "")
|
||||
}
|
||||
|
||||
if buf.is_empty() {
|
||||
remove_file(&path)
|
||||
.await
|
||||
.map_err(|e| ActionError::Remove(path.to_owned(), e))?;
|
||||
} else {
|
||||
file.seek(SeekFrom::Start(0))
|
||||
.await
|
||||
.map_err(|e| ActionError::Seek(path.to_owned(), e))?;
|
||||
file.write_all(file_contents.as_bytes())
|
||||
.await
|
||||
.map_err(|e| ActionError::Write(path.to_owned(), e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -13,21 +13,29 @@ use tokio::{
|
|||
};
|
||||
use tracing::{span, Span};
|
||||
|
||||
/** Create a file at the given location with the provided `buf`,
|
||||
optionally with an owning user, group, and mode.
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
|
||||
pub enum Position {
|
||||
Beginning,
|
||||
End,
|
||||
}
|
||||
|
||||
If the file exists, the provided `buf` will be inserted at its beginning.
|
||||
/** Create a file at the given location with the provided `buf` as
|
||||
contents, optionally with an owning user, group, and mode.
|
||||
|
||||
If the file exists, the provided `buf` will be inserted at its
|
||||
beginning or end, depending on the position field.
|
||||
*/
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||
pub struct CreateOrInsertFile {
|
||||
pub struct CreateOrInsertIntoFile {
|
||||
path: PathBuf,
|
||||
user: Option<String>,
|
||||
group: Option<String>,
|
||||
mode: Option<u32>,
|
||||
buf: String,
|
||||
position: Position,
|
||||
}
|
||||
|
||||
impl CreateOrInsertFile {
|
||||
impl CreateOrInsertIntoFile {
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub async fn plan(
|
||||
path: impl AsRef<Path>,
|
||||
|
@ -35,6 +43,7 @@ impl CreateOrInsertFile {
|
|||
group: impl Into<Option<String>>,
|
||||
mode: impl Into<Option<u32>>,
|
||||
buf: String,
|
||||
position: Position,
|
||||
) -> Result<StatefulAction<Self>, ActionError> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
|
||||
|
@ -44,14 +53,15 @@ impl CreateOrInsertFile {
|
|||
group: group.into(),
|
||||
mode: mode.into(),
|
||||
buf,
|
||||
position,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[typetag::serde(name = "create_or_insert_file")]
|
||||
impl Action for CreateOrInsertFile {
|
||||
#[typetag::serde(name = "create_or_insert_into_file")]
|
||||
impl Action for CreateOrInsertIntoFile {
|
||||
fn tracing_synopsis(&self) -> String {
|
||||
format!("Create or insert file `{}`", self.path.display())
|
||||
}
|
||||
|
@ -87,6 +97,7 @@ impl Action for CreateOrInsertFile {
|
|||
group,
|
||||
mode,
|
||||
buf,
|
||||
position
|
||||
} = self;
|
||||
|
||||
let mut orig_file = match OpenOptions::new().read(true).open(&path).await {
|
||||
|
@ -119,11 +130,27 @@ impl Action for CreateOrInsertFile {
|
|||
ActionError::Open(temp_file_path.clone(), e)
|
||||
})?;
|
||||
|
||||
if *position == Position::End {
|
||||
if let Some(ref mut orig_file) = orig_file {
|
||||
tokio::io::copy(orig_file, &mut temp_file)
|
||||
.await
|
||||
.map_err(|e| ActionError::Copy(path.to_owned(), temp_file_path.to_owned(), e))?;
|
||||
}
|
||||
}
|
||||
|
||||
temp_file
|
||||
.write_all(buf.as_bytes())
|
||||
.await
|
||||
.map_err(|e| ActionError::Write(temp_file_path.clone(), e))?;
|
||||
|
||||
if *position == Position::Beginning {
|
||||
if let Some(ref mut orig_file) = orig_file {
|
||||
tokio::io::copy(orig_file, &mut temp_file)
|
||||
.await
|
||||
.map_err(|e| ActionError::Copy(path.to_owned(), temp_file_path.to_owned(), e))?;
|
||||
}
|
||||
}
|
||||
|
||||
let gid = if let Some(group) = group {
|
||||
Some(
|
||||
Group::from_name(group.as_str())
|
||||
|
@ -145,12 +172,6 @@ impl Action for CreateOrInsertFile {
|
|||
None
|
||||
};
|
||||
|
||||
if let Some(ref mut orig_file) = orig_file {
|
||||
tokio::io::copy(orig_file, &mut temp_file)
|
||||
.await
|
||||
.map_err(|e| ActionError::Copy(path.to_owned(), temp_file_path.to_owned(), e))?;
|
||||
}
|
||||
|
||||
// Change ownership _before_ applying mode, to ensure that if
|
||||
// a file needs to be setuid it will never be setuid for the
|
||||
// wrong user
|
||||
|
@ -180,6 +201,7 @@ impl Action for CreateOrInsertFile {
|
|||
group: _,
|
||||
mode: _,
|
||||
buf,
|
||||
position: _,
|
||||
} = &self;
|
||||
vec![ActionDescription::new(
|
||||
format!("Delete Nix related fragment from file `{}`", path.display()),
|
||||
|
@ -198,6 +220,7 @@ impl Action for CreateOrInsertFile {
|
|||
group: _,
|
||||
mode: _,
|
||||
buf,
|
||||
position: _,
|
||||
} = self;
|
||||
let mut file = OpenOptions::new()
|
||||
.create(false)
|
||||
|
@ -205,12 +228,12 @@ impl Action for CreateOrInsertFile {
|
|||
.read(true)
|
||||
.open(&path)
|
||||
.await
|
||||
.map_err(|e| ActionError::Read(path.to_owned(), e))?;
|
||||
.map_err(|e| ActionError::Open(path.to_owned(), e))?;
|
||||
|
||||
let mut file_contents = String::default();
|
||||
file.read_to_string(&mut file_contents)
|
||||
.await
|
||||
.map_err(|e| ActionError::Seek(path.to_owned(), e))?;
|
||||
.map_err(|e| ActionError::Read(path.to_owned(), e))?;
|
||||
|
||||
if let Some(start) = file_contents.rfind(buf.as_str()) {
|
||||
let end = start + buf.len();
|
|
@ -3,8 +3,7 @@
|
|||
pub(crate) mod create_directory;
|
||||
pub(crate) mod create_file;
|
||||
pub(crate) mod create_group;
|
||||
pub(crate) mod create_or_append_file;
|
||||
pub(crate) mod create_or_insert_file;
|
||||
pub(crate) mod create_or_insert_into_file;
|
||||
pub(crate) mod create_user;
|
||||
pub(crate) mod fetch_and_unpack_nix;
|
||||
pub(crate) mod move_unpacked_nix;
|
||||
|
@ -13,8 +12,7 @@ pub(crate) mod setup_default_profile;
|
|||
pub use create_directory::CreateDirectory;
|
||||
pub use create_file::CreateFile;
|
||||
pub use create_group::CreateGroup;
|
||||
pub use create_or_append_file::CreateOrAppendFile;
|
||||
pub use create_or_insert_file::CreateOrInsertFile;
|
||||
pub use create_or_insert_into_file::CreateOrInsertIntoFile;
|
||||
pub use create_user::CreateUser;
|
||||
pub use fetch_and_unpack_nix::{FetchAndUnpackNix, FetchUrlError};
|
||||
pub use move_unpacked_nix::{MoveUnpackedNix, MoveUnpackedNixError};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::action::base::{CreateDirectory, CreateOrInsertFile};
|
||||
use crate::action::base::{CreateDirectory, CreateOrInsertIntoFile, create_or_insert_into_file};
|
||||
use crate::action::{Action, ActionDescription, ActionError, StatefulAction};
|
||||
|
||||
use nix::unistd::User;
|
||||
|
@ -36,7 +36,7 @@ Configure any detected shell profiles to include Nix support
|
|||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||
pub struct ConfigureShellProfile {
|
||||
create_directories: Vec<StatefulAction<CreateDirectory>>,
|
||||
create_or_insert_files: Vec<StatefulAction<CreateOrInsertFile>>,
|
||||
create_or_insert_into_files: Vec<StatefulAction<CreateOrInsertIntoFile>>,
|
||||
}
|
||||
|
||||
impl ConfigureShellProfile {
|
||||
|
@ -66,12 +66,13 @@ impl ConfigureShellProfile {
|
|||
continue;
|
||||
}
|
||||
create_or_insert_files.push(
|
||||
CreateOrInsertFile::plan(
|
||||
CreateOrInsertIntoFile::plan(
|
||||
profile_target_path,
|
||||
None,
|
||||
None,
|
||||
0o0755,
|
||||
shell_buf.to_string(),
|
||||
create_or_insert_into_file::Position::Beginning,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
@ -107,7 +108,7 @@ impl ConfigureShellProfile {
|
|||
}
|
||||
|
||||
create_or_insert_files.push(
|
||||
CreateOrInsertFile::plan(profile_target, None, None, 0o0755, fish_buf.to_string())
|
||||
CreateOrInsertIntoFile::plan(profile_target, None, None, 0o0755, fish_buf.to_string(), create_or_insert_into_file::Position::Beginning)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
@ -124,12 +125,12 @@ impl ConfigureShellProfile {
|
|||
);
|
||||
}
|
||||
create_or_insert_files
|
||||
.push(CreateOrInsertFile::plan(&github_path, None, None, None, buf).await?)
|
||||
.push(CreateOrInsertIntoFile::plan(&github_path, None, None, None, buf, create_or_insert_into_file::Position::End).await?)
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
create_directories,
|
||||
create_or_insert_files,
|
||||
create_or_insert_into_files: create_or_insert_files,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
@ -156,7 +157,7 @@ impl Action for ConfigureShellProfile {
|
|||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn execute(&mut self) -> Result<(), ActionError> {
|
||||
let Self {
|
||||
create_or_insert_files,
|
||||
create_or_insert_into_files,
|
||||
create_directories,
|
||||
} = self;
|
||||
|
||||
|
@ -167,22 +168,22 @@ impl Action for ConfigureShellProfile {
|
|||
let mut set = JoinSet::new();
|
||||
let mut errors = Vec::default();
|
||||
|
||||
for (idx, create_or_insert_file) in create_or_insert_files.iter().enumerate() {
|
||||
for (idx, create_or_insert_into_file) in create_or_insert_into_files.iter().enumerate() {
|
||||
let span = tracing::Span::current().clone();
|
||||
let mut create_or_insert_file_clone = create_or_insert_file.clone();
|
||||
let mut create_or_insert_into_file_clone = create_or_insert_into_file.clone();
|
||||
let _abort_handle = set.spawn(async move {
|
||||
create_or_insert_file_clone
|
||||
create_or_insert_into_file_clone
|
||||
.try_execute()
|
||||
.instrument(span)
|
||||
.await?;
|
||||
Result::<_, ActionError>::Ok((idx, create_or_insert_file_clone))
|
||||
Result::<_, ActionError>::Ok((idx, create_or_insert_into_file_clone))
|
||||
});
|
||||
}
|
||||
|
||||
while let Some(result) = set.join_next().await {
|
||||
match result {
|
||||
Ok(Ok((idx, create_or_insert_file))) => {
|
||||
create_or_insert_files[idx] = create_or_insert_file
|
||||
Ok(Ok((idx, create_or_insert_into_file))) => {
|
||||
create_or_insert_into_files[idx] = create_or_insert_into_file
|
||||
},
|
||||
Ok(Err(e)) => errors.push(Box::new(e)),
|
||||
Err(e) => return Err(e.into()),
|
||||
|
@ -211,14 +212,14 @@ impl Action for ConfigureShellProfile {
|
|||
async fn revert(&mut self) -> Result<(), ActionError> {
|
||||
let Self {
|
||||
create_directories,
|
||||
create_or_insert_files,
|
||||
create_or_insert_into_files,
|
||||
} = self;
|
||||
|
||||
let mut set = JoinSet::new();
|
||||
let mut errors: Vec<Box<ActionError>> = Vec::default();
|
||||
|
||||
for (idx, create_or_insert_file) in create_or_insert_files.iter().enumerate() {
|
||||
let mut create_or_insert_file_clone = create_or_insert_file.clone();
|
||||
for (idx, create_or_insert_into_file) in create_or_insert_into_files.iter().enumerate() {
|
||||
let mut create_or_insert_file_clone = create_or_insert_into_file.clone();
|
||||
let _abort_handle = set.spawn(async move {
|
||||
create_or_insert_file_clone.try_revert().await?;
|
||||
Result::<_, _>::Ok((idx, create_or_insert_file_clone))
|
||||
|
@ -227,8 +228,8 @@ impl Action for ConfigureShellProfile {
|
|||
|
||||
while let Some(result) = set.join_next().await {
|
||||
match result {
|
||||
Ok(Ok((idx, create_or_insert_file))) => {
|
||||
create_or_insert_files[idx] = create_or_insert_file
|
||||
Ok(Ok((idx, create_or_insert_into_file))) => {
|
||||
create_or_insert_into_files[idx] = create_or_insert_into_file
|
||||
},
|
||||
Ok(Err(e)) => errors.push(Box::new(e)),
|
||||
Err(e) => return Err(e.into()),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::action::{
|
||||
base::{CreateFile, CreateOrAppendFile},
|
||||
base::{CreateFile, CreateOrInsertIntoFile, create_or_insert_into_file},
|
||||
darwin::{
|
||||
BootstrapApfsVolume, CreateApfsVolume, CreateSyntheticObjects, EnableOwnership,
|
||||
EncryptApfsVolume, UnmountApfsVolume,
|
||||
|
@ -22,11 +22,11 @@ pub struct CreateNixVolume {
|
|||
name: String,
|
||||
case_sensitive: bool,
|
||||
encrypt: bool,
|
||||
create_or_append_synthetic_conf: StatefulAction<CreateOrAppendFile>,
|
||||
create_or_append_synthetic_conf: StatefulAction<CreateOrInsertIntoFile>,
|
||||
create_synthetic_objects: StatefulAction<CreateSyntheticObjects>,
|
||||
unmount_volume: StatefulAction<UnmountApfsVolume>,
|
||||
create_volume: StatefulAction<CreateApfsVolume>,
|
||||
create_or_append_fstab: StatefulAction<CreateOrAppendFile>,
|
||||
create_or_append_fstab: StatefulAction<CreateOrInsertIntoFile>,
|
||||
encrypt_volume: Option<StatefulAction<EncryptApfsVolume>>,
|
||||
setup_volume_daemon: StatefulAction<CreateFile>,
|
||||
bootstrap_volume: StatefulAction<BootstrapApfsVolume>,
|
||||
|
@ -42,12 +42,13 @@ impl CreateNixVolume {
|
|||
encrypt: bool,
|
||||
) -> Result<StatefulAction<Self>, ActionError> {
|
||||
let disk = disk.as_ref();
|
||||
let create_or_append_synthetic_conf = CreateOrAppendFile::plan(
|
||||
let create_or_append_synthetic_conf = CreateOrInsertIntoFile::plan(
|
||||
"/etc/synthetic.conf",
|
||||
None,
|
||||
None,
|
||||
0o0655,
|
||||
"nix\n".into(), /* The newline is required otherwise it segfaults */
|
||||
create_or_insert_into_file::Position::End,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ActionError::Child(Box::new(e)))?;
|
||||
|
@ -58,12 +59,13 @@ impl CreateNixVolume {
|
|||
|
||||
let create_volume = CreateApfsVolume::plan(disk, name.clone(), case_sensitive).await?;
|
||||
|
||||
let create_or_append_fstab = CreateOrAppendFile::plan(
|
||||
let create_or_append_fstab = CreateOrInsertIntoFile::plan(
|
||||
"/etc/fstab",
|
||||
None,
|
||||
None,
|
||||
0o0655,
|
||||
format!("NAME=\"{name}\" /nix apfs rw,noauto,nobrowse,suid,owners"),
|
||||
create_or_insert_into_file::Position::End,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ActionError::Child(Box::new(e)))?;
|
||||
|
|
Loading…
Reference in a new issue