Configuration.psm1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# Allows you to override the Scope storage paths (e.g. for testing)
param(
    $Converters     = @{},
    $EnterpriseData = $Env:AppData,
    $UserData       = $Env:LocalAppData,
    $MachineData    = $Env:ProgramData
)

$EnterpriseData = Join-Path $EnterpriseData WindowsPowerShell
$UserData       = Join-Path $UserData   WindowsPowerShell
$MachineData    = Join-Path $MachineData WindowsPowerShell

$ConfigurationRoot = Get-Variable PSScriptRoot* -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq "PSScriptRoot" } | ForEach-Object { $_.Value }
if(!$ConfigurationRoot) {
    $ConfigurationRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
}

Import-Module "${ConfigurationRoot}\Metadata.psm1" -Args @($Converters)

function Get-StoragePath {
    #.Synopsis
    # Gets an application storage path outside the module storage folder
    #.Description
    # Gets an AppData (or roaming profile) or ProgramData path for settings storage
    #
    # As a general rule, there are three scopes which result in three different root folders
    # User: $Env:LocalAppData
    # Machine: $Env:ProgramData
    # Enterprise: $Env:AppData (which is the "roaming" folder of AppData)
    #
    # WARNINGs:
    # 1. This command is only meant to be used in modules, to find a place where they can serialize data for storage. It can be used in scripts, but doing so is more risky.
    # 2. Since there are multiple module paths, it's possible for more than one module to exist with the same name, so you should exercise care
    #
    # If it doesn't already exist, the folder is created before the path is returned, so you can always trust this folder to exist.
    # The folder that is returned survives module uninstall/reinstall/upgrade, and this is the lowest level API for the Configuration module, expecting the module author to export data there using other Import/Export cmdlets.
    #.Example
    # $CacheFile = Join-Path (Get-StoragePath) Data.clixml
    # $Data | Export-CliXML -Path $CacheFile
    #
    # This example shows how to use Get-StoragePath with Export-CliXML to cache some data from inside a module.
    #
    [CmdletBinding(DefaultParameterSetName = 'NoParameters')]
    param(
        # The scope to save at, defaults to Enterprise (which returns a path in "RoamingData")
        [Security.PolicyLevelType]$Scope = "Enterprise",

        # A callstack. You should not ever need to pass this.
        # It is used to calculate the defaults for all the other parameters.
        [Parameter(ParameterSetName = "__CallStack")]
        [System.Management.Automation.CallStackFrame[]]$CallStack = $(Get-PSCallStack),

        # An optional module qualifier (by default, this is blank)
        [Parameter(ParameterSetName = "ManualOverride")]
        [String]$CompanyName = $(
            if($CallStack[0].InvocationInfo.MyCommand.Module){
                $Name = $CallStack[0].InvocationInfo.MyCommand.Module.CompanyName -replace "[$([Regex]::Escape(-join[IO.Path]::GetInvalidFileNameChars()))]","_"
                if($Name -eq "Unknown" -or -not $Name) {
                    $Name = $CallStack[0].InvocationInfo.MyCommand.Module.Author
                    if($Name -eq "Unknown" -or -not $Name) {
                        $Name = "AnonymousModules"
                    }
                }
                $Name
            } else {
                "AnonymousScripts"
            }
        ),

        # The name of the module or script
        # Will be used in the returned storage path
        [Parameter(ParameterSetName = "ManualOverride")]
        [String]$Name = $(
            if($Module = $CallStack[0].InvocationInfo.MyCommand.Module) {
                $Module.Name
            } else {
                if(!($BaseName = [IO.Path]::GetFileNameWithoutExtension(($CallStack[0].InvocationInfo.MyCommand.Name -replace "[$([Regex]::Escape(-join[IO.Path]::GetInvalidFileNameChars()))]","_")))) {
                    throw "Could not determine the storage name, Get-StoragePath should only be called from inside a script or module."
                }
                return $BaseName
            }
        ),

        # The version for saved settings -- if set, will be used in the returned path
        # NOTE: this is *NOT* calculated from the CallStack
        [Version]$Version
    )
    begin {
        $PathRoot = $(switch ($Scope) {
            "Enterprise" { $EnterpriseData }
            "User"       { $UserData }
            "Machine"    { $MachineData }
            # This should be "Process" scope, but what does that mean?
            # "AppDomain" { $MachineData }
            default { $EnterpriseData }
        })
    }

    end {
        $PathRoot = Join-Path $PathRoot $Type

        if($CompanyName -and $CompanyName -ne "Unknown") {
            $PathRoot = Join-Path $PathRoot $CompanyName
        }

        $PathRoot = Join-Path $PathRoot $Name

        if($Version) {
            $PathRoot = Join-Path $PathRoot $Version
        }

        # Note: avoid using Convert-Path because drives aliases like "TestData:" get converted to a C:\ file system location
        $null = mkdir $PathRoot -Force
        (Resolve-Path $PathRoot).Path
    }
}


function Import-Configuration {
    [CmdletBinding(DefaultParameterSetName = '__CallStack')]
    param(
        # A callstack. You should not ever need to pass this.
        # It is used to calculate the defaults for all the other parameters.
        [Parameter(ParameterSetName = "__CallStack")]
        [System.Management.Automation.CallStackFrame[]]$CallStack = $(Get-PSCallStack),

        # An optional module qualifier (by default, this is blank)
        [Parameter(ParameterSetName = "ManualOverride")]
        [Alias("Author")]
        [String]$CompanyName = $(
            if($CallStack[0].InvocationInfo.MyCommand.Module){
                $Name = $CallStack[0].InvocationInfo.MyCommand.Module.CompanyName -replace "[$([Regex]::Escape(-join[IO.Path]::GetInvalidFileNameChars()))]","_"
                if($Name -eq "Unknown" -or -not $Name) {
                    $Name = $CallStack[0].InvocationInfo.MyCommand.Module.Author
                    if($Name -eq "Unknown" -or -not $Name) {
                        $Name = "AnonymousModules"
                    }
                }
                $Name
            } else {
                "AnonymousScripts"
            }
        ),

        # The name of the module or script
        # Will be used in the returned storage path
        [Parameter(ParameterSetName = "ManualOverride", Mandatory=$true)]
        [String]$Name = $(
            if($Module = $CallStack[0].InvocationInfo.MyCommand.Module) {
                $Module.Name
            } else {
                if(!($BaseName = [IO.Path]::GetFileNameWithoutExtension(($CallStack[0].InvocationInfo.MyCommand.Name -replace "[$([Regex]::Escape(-join[IO.Path]::GetInvalidFileNameChars()))]","_")))) {
                    throw "Could not determine the storage name, Get-StoragePath should only be called from inside a script or module."
                }
                return $BaseName
            }
        ),

        # The full path to the module (in case there are dupes)
        # Will be used in the returned storage path
        [Parameter(ParameterSetName = "ManualOverride")]
        [String]$ModulePath = $(
            if($Module = $CallStack[0].InvocationInfo.MyCommand.Module) {
                $Module.Path
            } else {
                if(!($BaseName = [IO.Path]::GetFileNameWithoutExtension(($CallStack[0].InvocationInfo.MyCommand.Name -replace "[$([Regex]::Escape(-join[IO.Path]::GetInvalidFileNameChars()))]","_")))) {
                    throw "Could not determine the storage name, Get-StoragePath should only be called from inside a script or module."
                }
                return $BaseName
            }
        ),
        # The version for saved settings -- if set, will be used in the returned path
        # NOTE: this is *NOT* calculated from the CallStack
        [Version]$Version
    )

    Write-Verbose "PSBoundParameters $($PSBoundParameters | Out-String)"
    $ModulePath = Split-Path $ModulePath -Parent
    $ModulePath = Join-Path $ModulePath Configuration.psd1
    $Module = if(Test-Path $ModulePath) {
                Import-Metadata $ModulePath -ErrorAction Ignore
            } else { @{} }
    Write-Verbose "Module ($ModulePath)`n$($Module | Out-String)"

    $Parameters = @{
        CompanyName = $CompanyName
        Name = $Name
    }
    if($Version) {
        $Parameters.Version = $Version
    }

    $MachinePath = Get-StoragePath @Parameters -Scope Machine
    $MachinePath = Join-Path $MachinePath Configuration.psd1
    $Machine = if(Test-Path $MachinePath) {
                Import-Metadata $MachinePath -ErrorAction Ignore
            } else { @{} }
    Write-Verbose "Machine ($MachinePath)`n$($Machine | Out-String)"


    $EnterprisePath = Get-StoragePath @Parameters -Scope Enterprise
    $EnterprisePath = Join-Path $EnterprisePath Configuration.psd1
    $Enterprise = if(Test-Path $EnterprisePath) {
                Import-Metadata $EnterprisePath -ErrorAction Ignore
            } else { @{} }
    Write-Verbose "Enterprise ($EnterprisePath)`n$($Enterprise | Out-String)"

    $LocalUserPath = Get-StoragePath @Parameters -Scope User
    $LocalUserPath = Join-Path $LocalUserPath Configuration.psd1
    $LocalUser = if(Test-Path $LocalUserPath) {
                Import-Metadata $LocalUserPath -ErrorAction Ignore
            } else { @{} }
    Write-Verbose "LocalUser ($LocalUserPath)`n$($LocalUser | Out-String)"

    $Module | Update-Object $Machine | 
              Update-Object $Enterprise | 
              Update-Object $LocalUser
}