CreateOrInsertFile -> CreateOrInsertIntoFile

Also move appending behaviour in here.
This commit is contained in:
Linus Heckemann 2023-01-09 14:50:27 +01:00
parent 286f84c53f
commit d053e7fe69
5 changed files with 66 additions and 258 deletions

View file

@ -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(())
}
}

View file

@ -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();

View file

@ -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};

View file

@ -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()),

View file

@ -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)))?;