Skip to content

Commit 6de1ab8

Browse files
authored
Merge pull request #3718 from rina23q/improve/3693/add-immediate-parent-dir-control-to-config-mamagement
feat: create parent directories if missing for config_update operation
2 parents 723e135 + 59d5806 commit 6de1ab8

File tree

12 files changed

+361
-93
lines changed

12 files changed

+361
-93
lines changed

crates/common/tedge_utils/src/atomic.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use std::os::unix::fs::MetadataExt;
2020
use std::os::unix::fs::PermissionsExt;
2121
use std::path::Path;
2222

23+
use crate::file::PermissionEntry;
2324
use anyhow::Context;
2425

2526
/// Writes a file atomically and optionally sets its permissions.
@@ -112,12 +113,35 @@ fn target_permissions(dest: &Path, permissions: &MaybePermissions) -> anyhow::Re
112113
Ok(Permissions { uid, gid, mode })
113114
}
114115

116+
#[derive(Debug)]
115117
pub struct MaybePermissions {
116118
pub uid: Option<u32>,
117119
pub gid: Option<u32>,
118120
pub mode: Option<u32>,
119121
}
120122

123+
impl TryFrom<&PermissionEntry> for MaybePermissions {
124+
type Error = anyhow::Error;
125+
126+
fn try_from(value: &PermissionEntry) -> Result<Self, Self::Error> {
127+
let uid = value
128+
.user
129+
.as_ref()
130+
.map(|u| uzers::get_user_by_name(&u).with_context(|| format!("no such user: '{u}'")))
131+
.transpose()?
132+
.map(|u| u.uid());
133+
let gid = value
134+
.group
135+
.as_ref()
136+
.map(|g| uzers::get_group_by_name(&g).with_context(|| format!("no such group: '{g}'")))
137+
.transpose()?
138+
.map(|g| g.gid());
139+
let mode = value.mode;
140+
141+
Ok(Self { uid, gid, mode })
142+
}
143+
}
144+
121145
struct Permissions {
122146
uid: u32,
123147
gid: u32,

crates/common/tedge_utils/src/file.rs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ pub async fn create_directory<P: AsRef<Path>>(
7979
) -> Result<(), FileError> {
8080
permissions.create_directory(dir.as_ref()).await
8181
}
82+
8283
/// Create the directory owned by the user running this API with default directory permissions
8384
pub async fn create_directory_with_defaults<P: AsRef<Path>>(dir: P) -> Result<(), FileError> {
8485
create_directory(dir, &PermissionEntry::default()).await
@@ -264,6 +265,27 @@ impl PermissionEntry {
264265
Ok(())
265266
}
266267

268+
pub fn apply_sync(self, path: &Path) -> Result<(), FileError> {
269+
match (self.user, self.group) {
270+
(Some(user), Some(group)) => {
271+
change_user_and_group_sync(path, &user, &group)?;
272+
}
273+
(Some(user), None) => {
274+
change_user_sync(path, &user)?;
275+
}
276+
(None, Some(group)) => {
277+
change_group_sync(path, &group)?;
278+
}
279+
(None, None) => {}
280+
}
281+
282+
if let Some(mode) = &self.mode {
283+
change_mode_sync(path, *mode)?;
284+
}
285+
286+
Ok(())
287+
}
288+
267289
async fn create_directory(&self, dir: &Path) -> Result<(), FileError> {
268290
match dir.parent() {
269291
None => return Ok(()),
@@ -383,9 +405,9 @@ pub async fn change_user_and_group(
383405
.unwrap()
384406
}
385407

386-
pub fn change_user_and_group_sync(file: &Path, user: &str, group: &str) -> Result<(), FileError> {
387-
let metadata = get_metadata_sync(file)?;
388-
debug!("Changing ownership of file: {file:?} with user: {user} and group: {group}",);
408+
pub fn change_user_and_group_sync(path: &Path, user: &str, group: &str) -> Result<(), FileError> {
409+
let metadata = get_metadata_sync(path)?;
410+
debug!("Changing ownership of path: {path:?} with user: {user} and group: {group}",);
389411
let ud = get_user_by_name(&user)
390412
.map(|u| u.uid())
391413
.ok_or_else(|| FileError::UserNotFound {
@@ -405,9 +427,9 @@ pub fn change_user_and_group_sync(file: &Path, user: &str, group: &str) -> Resul
405427

406428
// if user and group are same as existing, then do not change
407429
if (ud != uid) || (gd != gid) {
408-
chown(file, Some(Uid::from_raw(ud)), Some(Gid::from_raw(gd))).map_err(|e| {
430+
chown(path, Some(Uid::from_raw(ud)), Some(Gid::from_raw(gd))).map_err(|e| {
409431
FileError::MetaDataError {
410-
name: file.display().to_string(),
432+
name: path.display().to_string(),
411433
from: e.into(),
412434
}
413435
})?;

crates/core/tedge_write/src/api.rs

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,8 @@ impl CopyOptions<'_> {
3535
///
3636
/// Stdin and Stdout are UTF-8.
3737
pub fn copy(self) -> anyhow::Result<()> {
38-
let mut command = self.command()?;
39-
40-
let output = command.output();
41-
42-
let program = command.get_program().to_string_lossy();
43-
let output = output.with_context(|| format!("failed to start process '{program}'"))?;
44-
45-
if !output.status.success() {
46-
let stderr = String::from_utf8_lossy(&output.stderr);
47-
let stderr = stderr.trim();
48-
let err = match output.status.code() {
49-
Some(exit_code) => anyhow!(
50-
"process '{program}' returned non-zero exit code ({exit_code}); stderr=\"{stderr}\""
51-
),
52-
None => anyhow!("process '{program}' was terminated; stderr=\"{stderr}\""),
53-
};
54-
55-
return Err(err);
56-
}
57-
58-
Ok(())
38+
let command = self.command()?;
39+
execute(command)
5940
}
6041

6142
fn command(&self) -> anyhow::Result<Command> {
@@ -83,6 +64,80 @@ impl CopyOptions<'_> {
8364
}
8465
}
8566

67+
/// Options for creating directories using a `tedge-write` process.
68+
#[derive(Debug, PartialEq)]
69+
pub struct CreateDirsOptions<'a> {
70+
/// Destination directory path
71+
pub dir_path: &'a Utf8Path,
72+
73+
/// User's sudo preference, received from TedgeConfig
74+
pub sudo: SudoCommandBuilder,
75+
76+
/// Permission mode for the immediate directory, in octal form.
77+
pub mode: Option<u32>,
78+
79+
/// User which will become the new owner of the immediate directory.
80+
pub user: Option<&'a str>,
81+
82+
/// Group which will become the new owner of the immediate directory.
83+
pub group: Option<&'a str>,
84+
}
85+
86+
impl CreateDirsOptions<'_> {
87+
/// Creates the directories by spawning new tedge-write process.
88+
///
89+
/// Stdout are UTF-8.
90+
pub fn create(self) -> anyhow::Result<()> {
91+
let command = self.command()?;
92+
execute(command)
93+
}
94+
95+
fn command(&self) -> anyhow::Result<Command> {
96+
// if tedge-write is in PATH of tedge process, use it, if not, defer PATH lookup to sudo
97+
let tedge_write_binary =
98+
which::which_global(TEDGE_WRITE_BINARY).unwrap_or(TEDGE_WRITE_BINARY.into());
99+
100+
let mut command = self.sudo.command(tedge_write_binary);
101+
102+
command.arg(self.dir_path);
103+
command.arg("--create-dirs-only");
104+
105+
if let Some(mode) = self.mode {
106+
command.arg("--parent-mode").arg(format!("{mode:o}"));
107+
}
108+
if let Some(user) = self.user {
109+
command.arg("--parent-user").arg(user);
110+
}
111+
if let Some(group) = self.group {
112+
command.arg("--parent-group").arg(group);
113+
}
114+
115+
Ok(command)
116+
}
117+
}
118+
119+
fn execute(mut command: Command) -> anyhow::Result<()> {
120+
let output = command.output();
121+
122+
let program = command.get_program().to_string_lossy();
123+
let output = output.with_context(|| format!("failed to start process '{program}'"))?;
124+
125+
if !output.status.success() {
126+
let stderr = String::from_utf8_lossy(&output.stderr);
127+
let stderr = stderr.trim();
128+
let err = match output.status.code() {
129+
Some(exit_code) => anyhow!(
130+
"process '{program}' returned non-zero exit code ({exit_code}); stderr=\"{stderr}\""
131+
),
132+
None => anyhow!("process '{program}' was terminated; stderr=\"{stderr}\""),
133+
};
134+
135+
return Err(err);
136+
}
137+
138+
Ok(())
139+
}
140+
86141
#[cfg(test)]
87142
mod tests {
88143
use std::os::unix::fs::PermissionsExt;

crates/core/tedge_write/src/bin.rs

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
use anyhow::bail;
44
use anyhow::Context;
5+
use camino::Utf8Path;
56
use camino::Utf8PathBuf;
7+
use clap::arg;
68
use clap::Parser;
79
use tedge_config::cli::CommonArgs;
810
use tedge_config::log_init;
911
use tedge_utils::atomic::MaybePermissions;
12+
use tedge_utils::file::PermissionEntry;
1013

1114
/// tee-like helper for writing to files which `tedge` user does not have write permissions to.
1215
///
@@ -19,6 +22,10 @@ pub struct Args {
1922
/// If the file does not exist, it will be created with the specified owner/group/permissions.
2023
/// If the file does exist, it will be overwritten, but its owner/group/permissions will remain
2124
/// unchanged.
25+
///
26+
/// If parent directories are missing, they will be created and the specified parent permissions
27+
/// will be applied only to the immediate parent.
28+
/// If the parents exist, they will remain unchanged.
2229
destination_path: Utf8PathBuf,
2330

2431
/// Permission mode for the file, in octal form.
@@ -33,6 +40,22 @@ pub struct Args {
3340
#[arg(long)]
3441
group: Option<Box<str>>,
3542

43+
/// Only create all parent directories if they are missing.
44+
#[arg(long)]
45+
create_dirs_only: bool,
46+
47+
/// Permission mode for the immediate parent directory, in octal form.
48+
#[arg(long)]
49+
parent_mode: Option<Box<str>>,
50+
51+
/// User which will become the new owner of the immediate parent directory.
52+
#[arg(long)]
53+
parent_user: Option<Box<str>>,
54+
55+
/// Group which will become the new owner of the immediate parent directory.
56+
#[arg(long)]
57+
parent_group: Option<Box<str>>,
58+
3659
#[command(flatten)]
3760
common: CommonArgs,
3861
}
@@ -60,45 +83,80 @@ pub fn run(args: Args) -> anyhow::Result<()> {
6083
}
6184

6285
// unwrap is safe because clean returns an utf8 path when given an utf8 path
63-
let target_filepath: Utf8PathBuf = path_clean::clean(args.destination_path.as_std_path())
86+
let target_path: Utf8PathBuf = path_clean::clean(args.destination_path.as_std_path())
6487
.try_into()
6588
.unwrap();
6689

67-
if target_filepath != *args.destination_path {
90+
if target_path != *args.destination_path {
6891
bail!(
6992
"Destination path {} is not canonical",
7093
args.destination_path
7194
);
7295
}
7396

74-
let mode = args
75-
.mode
97+
// Create the parent directories if they are missing
98+
if args.create_dirs_only {
99+
if !target_path.exists() {
100+
create_parent_dirs(&args, &target_path)?;
101+
}
102+
return Ok(());
103+
}
104+
105+
// what permissions we want to set if the file doesn't exist
106+
let file_permissions = get_permissions(args.mode, args.user, args.group)?;
107+
108+
let src = std::io::stdin().lock();
109+
110+
tedge_utils::atomic::write_file_atomic_set_permissions_if_doesnt_exist(
111+
src,
112+
&target_path,
113+
&file_permissions,
114+
)
115+
.with_context(|| format!("failed to write to destination file '{target_path}'"))?;
116+
117+
Ok(())
118+
}
119+
120+
fn create_parent_dirs(args: &Args, dir_path: &Utf8Path) -> anyhow::Result<()> {
121+
let parent_permissions = PermissionEntry::new(
122+
args.parent_user.clone().map(|s| s.into()),
123+
args.parent_group.clone().map(|s| s.into()),
124+
args.parent_mode
125+
.clone()
126+
.map(|m| u32::from_str_radix(&m, 8).with_context(|| format!("invalid mode: {m}")))
127+
.transpose()?,
128+
);
129+
130+
match std::fs::create_dir_all(dir_path) {
131+
Ok(_) => {
132+
parent_permissions.apply_sync(dir_path.as_std_path())?;
133+
}
134+
Err(err) => {
135+
bail!("failed to create parent directories. path: '{dir_path}', error: '{err}'");
136+
}
137+
}
138+
139+
Ok(())
140+
}
141+
142+
fn get_permissions(
143+
mode: Option<Box<str>>,
144+
user: Option<Box<str>>,
145+
group: Option<Box<str>>,
146+
) -> anyhow::Result<MaybePermissions> {
147+
let mode = mode
76148
.map(|m| u32::from_str_radix(&m, 8).with_context(|| format!("invalid mode: {m}")))
77149
.transpose()?;
78150

79-
let uid = args
80-
.user
151+
let uid = user
81152
.map(|u| uzers::get_user_by_name(&*u).with_context(|| format!("no such user: '{u}'")))
82153
.transpose()?
83154
.map(|u| u.uid());
84155

85-
let gid = args
86-
.group
156+
let gid = group
87157
.map(|g| uzers::get_group_by_name(&*g).with_context(|| format!("no such group: '{g}'")))
88158
.transpose()?
89159
.map(|g| g.gid());
90160

91-
// what permissions we want to set if the file doesn't exist
92-
let permissions = MaybePermissions { uid, gid, mode };
93-
94-
let src = std::io::stdin().lock();
95-
96-
tedge_utils::atomic::write_file_atomic_set_permissions_if_doesnt_exist(
97-
src,
98-
&target_filepath,
99-
&permissions,
100-
)
101-
.with_context(|| format!("failed to write to destination file '{target_filepath}'"))?;
102-
103-
Ok(())
161+
Ok(MaybePermissions { uid, gid, mode })
104162
}

crates/core/tedge_write/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ pub mod bin;
3030
mod api;
3131

3232
pub use api::CopyOptions;
33+
pub use api::CreateDirsOptions;

0 commit comments

Comments
 (0)