Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion resources/filecontent/filecontent.dsc.resource.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions resources/filecontent/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
26 changes: 25 additions & 1 deletion resources/filecontent/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -37,6 +38,24 @@ pub fn set(input: &FileContent) -> Result<FileContent, String> {
};
validate_content_hashes(input, content)?;

if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
&& !parent.is_dir()
{
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()
Comment thread
SteveL-MSFT marked this conversation as resolved.
)
.to_string()
})?;
}

fs::write(path, content.as_bytes()).map_err(|error| {
t!(
"set.writeError",
Expand Down Expand Up @@ -136,7 +155,12 @@ fn validate_hash(value: Option<&str>, length: usize, name: &str) -> Result<(), S
fn read_state(path: &str) -> Result<FileContent, String> {
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,
Expand Down
51 changes: 49 additions & 2 deletions resources/filecontent/tests/filecontent_set.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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' {
Expand All @@ -26,6 +28,51 @@ Describe 'FileContent set tests' {
$actual.sha256 | Should -BeExactly '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
}

It 'Creates missing parent directories and reports the creation when _exist is <existSetting>' -ForEach @(
@{ existSetting = 'true'; includeExist = $true }
@{ existSetting = 'omitted'; includeExist = $false }
) {
$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) {
$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'
$firstMissingDirectory | Should -Exist
$secondMissingDirectory | Should -Exist
$actual._exist | Should -BeTrue
(Get-Content -Raw $stderrPath) |
Should -BeLike "*INFO*Creating parent directory '$secondMissingDirectory'*"
}

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

$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' {
[System.IO.File]::WriteAllText($filePath, 'remove me')
$json = @{ path = $filePath; _exist = $false } | ConvertTo-Json -Compress
Expand Down
Loading