From 64edaec86ca301fb3bb71b13be07eaeebf1fe2a5 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Mon, 21 Sep 2026 15:50:14 -0700 Subject: [PATCH 1/5] Create parent directories for FileContent Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../filecontent/filecontent.dsc.resource.json | 2 +- resources/filecontent/locales/en-us.toml | 2 ++ resources/filecontent/src/file.rs | 19 ++++++++++++ .../tests/filecontent_set.tests.ps1 | 29 +++++++++++++++++-- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/resources/filecontent/filecontent.dsc.resource.json b/resources/filecontent/filecontent.dsc.resource.json index 12ba48307..514a256fe 100644 --- a/resources/filecontent/filecontent.dsc.resource.json +++ b/resources/filecontent/filecontent.dsc.resource.json @@ -84,7 +84,7 @@ "_exist": { "type": "boolean", "title": "Exists", - "description": "Indicates whether the file should exist. Set to false to remove the file." + "description": "Indicates whether the file should exist. Set to true or omit to create or update the file and any missing parent directories. Set to false to remove the file." }, "_inDesiredState": { "type": "boolean", diff --git a/resources/filecontent/locales/en-us.toml b/resources/filecontent/locales/en-us.toml index c2a3834ae..ce267e0ca 100644 --- a/resources/filecontent/locales/en-us.toml +++ b/resources/filecontent/locales/en-us.toml @@ -17,6 +17,8 @@ readError = "Failed to read file '%{path}': %{error}" [set] contentRequired = "The content property is required when setting a file" +creatingParentDirectory = "Creating parent directory '%{path}'" +createParentDirectoryError = "Failed to create parent directory '%{path}': %{error}" writeError = "Failed to write file '%{path}': %{error}" removeError = "Failed to remove file '%{path}': %{error}" sha256Mismatch = "The sha256 value does not match the content property" diff --git a/resources/filecontent/src/file.rs b/resources/filecontent/src/file.rs index d6a8306b4..905750aa9 100644 --- a/resources/filecontent/src/file.rs +++ b/resources/filecontent/src/file.rs @@ -3,6 +3,7 @@ use crate::types::FileContent; use rust_i18n::t; +use serde_json::json; use sha2::{Digest, Sha256, Sha512}; use std::fs; use std::path::Path; @@ -37,6 +38,24 @@ pub fn set(input: &FileContent) -> Result { }; validate_content_hashes(input, content)?; + if let Some(parent) = path.parent() + && !parent.as_os_str().is_empty() + && !parent.exists() + { + eprintln!( + "{}", + json!({ "info": t!("set.creatingParentDirectory", path = parent.display().to_string()) }) + ); + fs::create_dir_all(parent).map_err(|error| { + t!( + "set.createParentDirectoryError", + path = parent.display().to_string(), + error = error.to_string() + ) + .to_string() + })?; + } + fs::write(path, content.as_bytes()).map_err(|error| { t!( "set.writeError", diff --git a/resources/filecontent/tests/filecontent_set.tests.ps1 b/resources/filecontent/tests/filecontent_set.tests.ps1 index 4cd42dad4..bfddb1a2a 100644 --- a/resources/filecontent/tests/filecontent_set.tests.ps1 +++ b/resources/filecontent/tests/filecontent_set.tests.ps1 @@ -7,11 +7,13 @@ Describe 'FileContent set tests' { } BeforeEach { - $filePath = Join-Path $TestDrive "$([System.Guid]::NewGuid()).txt" + $testRoot = Join-Path $TestDrive "$([System.Guid]::NewGuid())" + $null = New-Item -ItemType Directory -Path $testRoot + $filePath = Join-Path $testRoot 'file.txt' } AfterEach { - Remove-Item -LiteralPath $filePath -Force -ErrorAction Ignore + Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction Ignore } It 'Creates a UTF-8 file and returns its hashes' { @@ -26,6 +28,29 @@ Describe 'FileContent set tests' { $actual.sha256 | Should -BeExactly '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824' } + It 'Creates missing parent directories and reports the creation when _exist is ' -ForEach @( + @{ existSetting = 'true'; includeExist = $true } + @{ existSetting = 'omitted'; includeExist = $false } + ) { + $nestedDirectory = Join-Path (Join-Path $testRoot 'first') 'second' + $nestedFilePath = Join-Path $nestedDirectory 'file.txt' + $stderrPath = Join-Path $testRoot 'stderr.log' + $inputState = @{ path = $nestedFilePath; content = 'nested' } + if ($includeExist) { + $inputState._exist = $true + } + $json = $inputState | ConvertTo-Json -Compress + + $out = $json | dsc -l info resource set -r $resourceType -f - 2>$stderrPath + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw $stderrPath) + $actual = ($out | ConvertFrom-Json).afterState + + [System.IO.File]::ReadAllText($nestedFilePath) | Should -BeExactly 'nested' + $actual._exist | Should -BeTrue + (Get-Content -Raw $stderrPath) | + Should -Match ([regex]::Escape("Creating parent directory '$nestedDirectory'")) + } + It 'Removes a file when _exist is false' { [System.IO.File]::WriteAllText($filePath, 'remove me') $json = @{ path = $filePath; _exist = $false } | ConvertTo-Json -Compress From 280b3d01e274f8fb2a3ad797109e12fb8d33aecd Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Mon, 21 Sep 2026 16:03:28 -0700 Subject: [PATCH 2/5] Expand FileContent directory tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../filecontent/tests/filecontent_set.tests.ps1 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/resources/filecontent/tests/filecontent_set.tests.ps1 b/resources/filecontent/tests/filecontent_set.tests.ps1 index bfddb1a2a..f8649b622 100644 --- a/resources/filecontent/tests/filecontent_set.tests.ps1 +++ b/resources/filecontent/tests/filecontent_set.tests.ps1 @@ -48,7 +48,22 @@ Describe 'FileContent set tests' { [System.IO.File]::ReadAllText($nestedFilePath) | Should -BeExactly 'nested' $actual._exist | Should -BeTrue (Get-Content -Raw $stderrPath) | - Should -Match ([regex]::Escape("Creating parent directory '$nestedDirectory'")) + Should -BeLike "*INFO*Creating parent directory '$nestedDirectory'*" + } + + It 'Reports an error when a file blocks parent directory creation' { + $blockingPath = Join-Path $testRoot 'blocked' + [System.IO.File]::WriteAllText($blockingPath, 'blocking file') + $blockedParent = Join-Path $blockingPath 'child' + $blockedFilePath = Join-Path $blockedParent 'file.txt' + $stderrPath = Join-Path $testRoot 'stderr.log' + $json = @{ path = $blockedFilePath; content = 'blocked' } | ConvertTo-Json -Compress + + $null = $json | dsc resource set -r $resourceType -f - 2>$stderrPath + + $LASTEXITCODE | Should -Not -Be 0 + (Get-Content -Raw $stderrPath) | + Should -BeLike "*Failed to create parent directory '$blockedParent'*" } It 'Removes a file when _exist is false' { From e2f17c6eec9262b2678190bb31634ecfc2a38493 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Mon, 21 Sep 2026 16:16:36 -0700 Subject: [PATCH 3/5] Handle blocked parent paths consistently Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- resources/filecontent/src/file.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/filecontent/src/file.rs b/resources/filecontent/src/file.rs index 905750aa9..5da527bae 100644 --- a/resources/filecontent/src/file.rs +++ b/resources/filecontent/src/file.rs @@ -155,7 +155,12 @@ fn validate_hash(value: Option<&str>, length: usize, name: &str) -> Result<(), S fn read_state(path: &str) -> Result { let bytes = match fs::read(path) { Ok(bytes) => bytes, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory + ) => + { return Ok(FileContent { path: path.to_string(), content: None, From 9d3f7fa82d6cab1d012a3bc5876dc92cc22a4bf9 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Mon, 21 Sep 2026 18:53:19 -0700 Subject: [PATCH 4/5] Validate FileContent parent directories Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- resources/filecontent/src/file.rs | 2 +- resources/filecontent/tests/filecontent_set.tests.ps1 | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/resources/filecontent/src/file.rs b/resources/filecontent/src/file.rs index 5da527bae..9072b1e2a 100644 --- a/resources/filecontent/src/file.rs +++ b/resources/filecontent/src/file.rs @@ -40,7 +40,7 @@ pub fn set(input: &FileContent) -> Result { if let Some(parent) = path.parent() && !parent.as_os_str().is_empty() - && !parent.exists() + && !parent.is_dir() { eprintln!( "{}", diff --git a/resources/filecontent/tests/filecontent_set.tests.ps1 b/resources/filecontent/tests/filecontent_set.tests.ps1 index f8649b622..7275664c5 100644 --- a/resources/filecontent/tests/filecontent_set.tests.ps1 +++ b/resources/filecontent/tests/filecontent_set.tests.ps1 @@ -52,9 +52,8 @@ Describe 'FileContent set tests' { } It 'Reports an error when a file blocks parent directory creation' { - $blockingPath = Join-Path $testRoot 'blocked' - [System.IO.File]::WriteAllText($blockingPath, 'blocking file') - $blockedParent = Join-Path $blockingPath 'child' + $blockedParent = Join-Path $testRoot 'blocked' + [System.IO.File]::WriteAllText($blockedParent, 'blocking file') $blockedFilePath = Join-Path $blockedParent 'file.txt' $stderrPath = Join-Path $testRoot 'stderr.log' $json = @{ path = $blockedFilePath; content = 'blocked' } | ConvertTo-Json -Compress From d83841000e67dd3032f486af0de7241996cd19d8 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Mon, 21 Sep 2026 21:13:37 -0700 Subject: [PATCH 5/5] Test multi-level FileContent paths Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../tests/filecontent_set.tests.ps1 | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/resources/filecontent/tests/filecontent_set.tests.ps1 b/resources/filecontent/tests/filecontent_set.tests.ps1 index 7275664c5..59ced792a 100644 --- a/resources/filecontent/tests/filecontent_set.tests.ps1 +++ b/resources/filecontent/tests/filecontent_set.tests.ps1 @@ -32,8 +32,11 @@ Describe 'FileContent set tests' { @{ existSetting = 'true'; includeExist = $true } @{ existSetting = 'omitted'; includeExist = $false } ) { - $nestedDirectory = Join-Path (Join-Path $testRoot 'first') 'second' - $nestedFilePath = Join-Path $nestedDirectory 'file.txt' + $existingDirectory = Join-Path $testRoot 'a' + $null = New-Item -ItemType Directory -Path $existingDirectory + $firstMissingDirectory = Join-Path $existingDirectory 'b' + $secondMissingDirectory = Join-Path $firstMissingDirectory 'c' + $nestedFilePath = Join-Path $secondMissingDirectory 'file.txt' $stderrPath = Join-Path $testRoot 'stderr.log' $inputState = @{ path = $nestedFilePath; content = 'nested' } if ($includeExist) { @@ -46,14 +49,19 @@ Describe 'FileContent set tests' { $actual = ($out | ConvertFrom-Json).afterState [System.IO.File]::ReadAllText($nestedFilePath) | Should -BeExactly 'nested' + $firstMissingDirectory | Should -Exist + $secondMissingDirectory | Should -Exist $actual._exist | Should -BeTrue (Get-Content -Raw $stderrPath) | - Should -BeLike "*INFO*Creating parent directory '$nestedDirectory'*" + Should -BeLike "*INFO*Creating parent directory '$secondMissingDirectory'*" } - It 'Reports an error when a file blocks parent directory creation' { - $blockedParent = Join-Path $testRoot 'blocked' - [System.IO.File]::WriteAllText($blockedParent, 'blocking file') + It 'Reports an error when an intermediate parent path is a file' { + $existingDirectory = Join-Path $testRoot 'a' + $null = New-Item -ItemType Directory -Path $existingDirectory + $blockingPath = Join-Path $existingDirectory 'b' + [System.IO.File]::WriteAllText($blockingPath, 'blocking file') + $blockedParent = Join-Path $blockingPath 'c' $blockedFilePath = Join-Path $blockedParent 'file.txt' $stderrPath = Join-Path $testRoot 'stderr.log' $json = @{ path = $blockedFilePath; content = 'blocked' } | ConvertTo-Json -Compress