Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
param(
|
||||
[string]$Server = $env:ONEC_SQL_SERVER,
|
||||
[string]$Database = $env:ONEC_SQL_DATABASE,
|
||||
[string]$User = $env:ONEC_SQL_USER,
|
||||
[string]$Password = $env:ONEC_SQL_PASSWORD,
|
||||
[string]$BaseMetadataDir = "reports\1c-sql\upo\structured-metadata-all-kinds",
|
||||
[string]$ExtensionGuidIndex = "reports\1c-sql\upo\xml-guid-index-extensions.json",
|
||||
[string]$Output
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not $Server) { throw "Server is required. Use -Server or ONEC_SQL_SERVER." }
|
||||
if (-not $Database) { throw "Database is required. Use -Database or ONEC_SQL_DATABASE." }
|
||||
if (-not $User) { throw "User is required. Use -User or ONEC_SQL_USER." }
|
||||
if (-not $Password) { throw "Password is required. Use -Password or ONEC_SQL_PASSWORD." }
|
||||
|
||||
function U {
|
||||
param([string]$Base64)
|
||||
return [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Base64))
|
||||
}
|
||||
|
||||
$ruExtension = U "0KDQsNGB0YjQuNGA0LXQvdC40LU="
|
||||
$ruConfigObject = U "0J7QsdGK0LXQutGC0JrQvtC90YTQuNCz0YPRgNCw0YbQuNC4"
|
||||
$ruForm = U "0KTQvtGA0LzQsA=="
|
||||
|
||||
$kindRu = @{
|
||||
Configuration = (U "0JrQvtC90YTQuNCz0YPRgNCw0YbQuNGP")
|
||||
CommonModule = (U "0J7QsdGJ0LjQudCc0L7QtNGD0LvRjA==")
|
||||
CommonForm = (U "0J7QsdGJ0LDRj9Ck0L7RgNC80LA=")
|
||||
CommonCommand = (U "0J7QsdGJ0LDRj9Ca0L7QvNCw0L3QtNCw")
|
||||
CommonAttribute = (U "0J7QsdGJ0LjQudCg0LXQutCy0LjQt9C40YI=")
|
||||
CommonTemplate = (U "0J7QsdGJ0LjQudCc0LDQutC10YI=")
|
||||
CommonPicture = (U "0J7QsdGJ0LDRj9Ca0LDRgNGC0LjQvdC60LA=")
|
||||
Catalog = (U "0KHQv9GA0LDQstC+0YfQvdC40Lo=")
|
||||
Document = (U "0JTQvtC60YPQvNC10L3Rgg==")
|
||||
DataProcessor = (U "0J7QsdGA0LDQsdC+0YLQutCw")
|
||||
Report = (U "0J7RgtGH0LXRgg==")
|
||||
InformationRegister = (U "0KDQtdCz0LjRgdGC0YDQodCy0LXQtNC10L3QuNC5")
|
||||
AccumulationRegister = (U "0KDQtdCz0LjRgdGC0YDQndCw0LrQvtC/0LvQtdC90LjRjw==")
|
||||
AccountingRegister = (U "0KDQtdCz0LjRgdGC0YDQkdGD0YXQs9Cw0LvRgtC10YDQuNC4")
|
||||
CalculationRegister = (U "0KDQtdCz0LjRgdGC0YDQoNCw0YHRh9C10YLQsA==")
|
||||
Enum = (U "0J/QtdGA0LXRh9C40YHQu9C10L3QuNC1")
|
||||
Form = $ruForm
|
||||
Template = (U "0JzQsNC60LXRgg==")
|
||||
Role = (U "0KDQvtC70Yw=")
|
||||
Subsystem = (U "0J/QvtC00YHQuNGB0YLQtdC80LA=")
|
||||
ExchangePlan = (U "0J/Qu9Cw0L3QntCx0LzQtdC90LA=")
|
||||
BusinessProcess = (U "0JHQuNC30L3QtdGB0J/RgNC+0YbQtdGB0YE=")
|
||||
Task = (U "0JfQsNC00LDRh9Cw")
|
||||
Constant = (U "0JrQvtC90YHRgtCw0L3RgtCw")
|
||||
ChartOfCharacteristicTypes = (U "0J/Qu9Cw0L3QktC40LTQvtCy0KXQsNGA0LDQutGC0LXRgNC40YHRgtC40Lo=")
|
||||
ChartOfAccounts = (U "0J/Qu9Cw0L3QodGH0LXRgtC+0LI=")
|
||||
ChartOfCalculationTypes = (U "0J/Qu9Cw0L3QktC40LTQvtCy0KDQsNGB0YfQtdGC0LA=")
|
||||
}
|
||||
|
||||
$folderKind = @{
|
||||
CommonModules = "CommonModule"
|
||||
CommonForms = "CommonForm"
|
||||
CommonCommands = "CommonCommand"
|
||||
CommonAttributes = "CommonAttribute"
|
||||
CommonTemplates = "CommonTemplate"
|
||||
CommonPictures = "CommonPicture"
|
||||
Catalogs = "Catalog"
|
||||
Documents = "Document"
|
||||
DataProcessors = "DataProcessor"
|
||||
Reports = "Report"
|
||||
InformationRegisters = "InformationRegister"
|
||||
AccumulationRegisters = "AccumulationRegister"
|
||||
AccountingRegisters = "AccountingRegister"
|
||||
CalculationRegisters = "CalculationRegister"
|
||||
Enums = "Enum"
|
||||
Forms = "Form"
|
||||
Templates = "Template"
|
||||
Roles = "Role"
|
||||
Subsystems = "Subsystem"
|
||||
ExchangePlans = "ExchangePlan"
|
||||
BusinessProcesses = "BusinessProcess"
|
||||
Tasks = "Task"
|
||||
Constants = "Constant"
|
||||
ChartsOfCharacteristicTypes = "ChartOfCharacteristicTypes"
|
||||
ChartsOfAccounts = "ChartOfAccounts"
|
||||
ChartsOfCalculationTypes = "ChartOfCalculationTypes"
|
||||
}
|
||||
|
||||
function Convert-RefBytesToGuid {
|
||||
param([byte[]]$Bytes)
|
||||
if ($Bytes.Length -ne 16) { return $null }
|
||||
$hex = ($Bytes | ForEach-Object { $_.ToString("x2") }) -join ""
|
||||
return "{0}-{1}-{2}-{3}-{4}" -f $hex.Substring(24, 8), $hex.Substring(20, 4), $hex.Substring(16, 4), $hex.Substring(0, 4), $hex.Substring(4, 12)
|
||||
}
|
||||
|
||||
function Convert-ToSha256Hex {
|
||||
param([byte[]]$Bytes)
|
||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
return (($sha.ComputeHash($Bytes) | ForEach-Object { $_.ToString("x2") }) -join "")
|
||||
}
|
||||
finally {
|
||||
$sha.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Join-ByteArrays {
|
||||
param([object[]]$Rows)
|
||||
$ordered = @($Rows | Sort-Object part_no)
|
||||
$total = 0
|
||||
foreach ($row in $ordered) { $total += $row.bytes.Length }
|
||||
$buffer = [byte[]]::new($total)
|
||||
$offset = 0
|
||||
foreach ($row in $ordered) {
|
||||
[Array]::Copy($row.bytes, 0, $buffer, $offset, $row.bytes.Length)
|
||||
$offset += $row.bytes.Length
|
||||
}
|
||||
return $buffer
|
||||
}
|
||||
|
||||
function New-Connection {
|
||||
$connectionString = "Server=$Server;Database=$Database;User ID=$User;Password=$Password;Encrypt=False;TrustServerCertificate=True;MultipleActiveResultSets=True;Application Name=Codex 1C Saved State Object Compare;"
|
||||
$connection = [System.Data.SqlClient.SqlConnection]::new($connectionString)
|
||||
$connection.Open()
|
||||
return $connection
|
||||
}
|
||||
|
||||
function Read-TableFiles {
|
||||
param(
|
||||
[System.Data.SqlClient.SqlConnection]$Connection,
|
||||
[string]$Table,
|
||||
[string[]]$FileNames
|
||||
)
|
||||
$command = $Connection.CreateCommand()
|
||||
if ($FileNames -and $FileNames.Count -gt 0) {
|
||||
$placeholders = @()
|
||||
for ($i = 0; $i -lt $FileNames.Count; $i++) {
|
||||
$paramName = "@p$i"
|
||||
$placeholders += $paramName
|
||||
$null = $command.Parameters.Add($paramName, [System.Data.SqlDbType]::NVarChar, 512)
|
||||
$command.Parameters[$paramName].Value = $FileNames[$i]
|
||||
}
|
||||
$command.CommandText = "SELECT FileName, PartNo, BinaryData FROM dbo.[$Table] WHERE FileName IN ($($placeholders -join ', ')) ORDER BY FileName, PartNo"
|
||||
}
|
||||
else {
|
||||
$command.CommandText = "SELECT FileName, PartNo, BinaryData FROM dbo.[$Table] ORDER BY FileName, PartNo"
|
||||
}
|
||||
$reader = $null
|
||||
$groups = @{}
|
||||
try {
|
||||
$reader = $command.ExecuteReader()
|
||||
while ($reader.Read()) {
|
||||
$fileName = [string]$reader.GetValue(0)
|
||||
if (-not $groups.ContainsKey($fileName)) { $groups[$fileName] = @() }
|
||||
$groups[$fileName] += [pscustomobject]@{
|
||||
part_no = [int]$reader.GetValue(1)
|
||||
bytes = [byte[]]$reader.GetValue(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($reader -ne $null) { $reader.Dispose() }
|
||||
if ($command -ne $null) { $command.Dispose() }
|
||||
}
|
||||
|
||||
$result = @{}
|
||||
foreach ($fileName in $groups.Keys) {
|
||||
$bytes = Join-ByteArrays @($groups[$fileName])
|
||||
$result[$fileName] = [pscustomobject]@{
|
||||
file_name = $fileName
|
||||
chunks = @($groups[$fileName]).Count
|
||||
bytes = $bytes.Length
|
||||
sha256 = Convert-ToSha256Hex $bytes
|
||||
}
|
||||
}
|
||||
return $result
|
||||
}
|
||||
|
||||
function Read-Extensions {
|
||||
param([System.Data.SqlClient.SqlConnection]$Connection)
|
||||
$command = $Connection.CreateCommand()
|
||||
$command.CommandText = "SELECT [_IDRRef], [_ExtName], [_ExtensionOrder], [_ExtensionUsePurpose], [_ExtensionScope] FROM dbo.[_ExtensionsInfo] ORDER BY [_ExtensionOrder], [_ExtName]"
|
||||
$reader = $null
|
||||
$map = @{}
|
||||
try {
|
||||
$reader = $command.ExecuteReader()
|
||||
while ($reader.Read()) {
|
||||
$bytes = [byte[]]$reader.GetValue(0)
|
||||
$guid = Convert-RefBytesToGuid $bytes
|
||||
$map[$guid] = [pscustomobject]@{
|
||||
guid = $guid
|
||||
name = [string]$reader.GetValue(1)
|
||||
order = [decimal]$reader.GetValue(2)
|
||||
use_purpose = [decimal]$reader.GetValue(3)
|
||||
scope = [decimal]$reader.GetValue(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($reader -ne $null) { $reader.Dispose() }
|
||||
if ($command -ne $null) { $command.Dispose() }
|
||||
}
|
||||
return $map
|
||||
}
|
||||
|
||||
function Get-BaseObjectInfo {
|
||||
param([string]$Guid)
|
||||
$metadataRoot = [System.IO.Path]::GetFullPath($BaseMetadataDir)
|
||||
if (-not (Test-Path -LiteralPath $metadataRoot)) { return $null }
|
||||
$match = Get-ChildItem -LiteralPath $metadataRoot -Recurse -Filter *.json -File |
|
||||
Select-String -SimpleMatch $Guid -List |
|
||||
Select-Object -First 1
|
||||
if (-not $match) { return $null }
|
||||
$data = Get-Content -LiteralPath $match.Path -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$kind = [string]$data.kind
|
||||
$name = [string]$data.identity.name
|
||||
$synonym = $null
|
||||
if ($data.identity.synonyms -and $data.identity.synonyms.ru) { $synonym = [string]$data.identity.synonyms.ru }
|
||||
return [pscustomobject]@{
|
||||
layer = "base"
|
||||
kind = $kind
|
||||
kind_ru = if ($kindRu.ContainsKey($kind)) { $kindRu[$kind] } else { $kind }
|
||||
name = $name
|
||||
synonym = $synonym
|
||||
full_name = "$(if ($kindRu.ContainsKey($kind)) { $kindRu[$kind] } else { $kind }).$name"
|
||||
guid = $Guid
|
||||
evidence = [pscustomobject]@{
|
||||
metadata_file = $match.Path
|
||||
xml_file = $data.xml_file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ExtensionGuidMap {
|
||||
$path = [System.IO.Path]::GetFullPath($ExtensionGuidIndex)
|
||||
if (-not (Test-Path -LiteralPath $path)) { return @{} }
|
||||
$data = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
return $data.guid_map
|
||||
}
|
||||
|
||||
function Get-ExtensionObjectInfo {
|
||||
param(
|
||||
[string]$ExtensionName,
|
||||
[string]$ObjectGuid,
|
||||
[object]$GuidMap
|
||||
)
|
||||
$entry = $GuidMap.$ObjectGuid
|
||||
$top = $null
|
||||
if ($entry -and $entry.top_objects -and @($entry.top_objects).Count -gt 0) {
|
||||
$top = @($entry.top_objects)[0]
|
||||
}
|
||||
$relativePath = if ($top) { [string]$top.relative_path } else { $null }
|
||||
$parts = if ($relativePath) { $relativePath -split "[\\/]" } else { @() }
|
||||
$parentKind = $null
|
||||
$parentName = $null
|
||||
$artifactKind = if ($top) { [string]$top.xml_kind } else { "ConfigCASObject" }
|
||||
$artifactName = if ($top) { [string]$top.name } else { $ObjectGuid }
|
||||
$synonym = if ($top -and $top.synonym) { [string]$top.synonym } else { $null }
|
||||
for ($i = 0; $i -lt $parts.Count - 1; $i++) {
|
||||
if ($folderKind.ContainsKey($parts[$i]) -and $parts[$i] -ne "Forms" -and $parts[$i] -ne "Templates") {
|
||||
$parentKind = $folderKind[$parts[$i]]
|
||||
if ($i + 1 -lt $parts.Count) { $parentName = $parts[$i + 1] }
|
||||
break
|
||||
}
|
||||
}
|
||||
$artifactKindRu = if ($kindRu.ContainsKey($artifactKind)) { $kindRu[$artifactKind] } else { $artifactKind }
|
||||
$parentKindRu = if ($parentKind -and $kindRu.ContainsKey($parentKind)) { $kindRu[$parentKind] } else { $parentKind }
|
||||
$fullName = if ($parentKind -and $artifactKind -eq "Form") {
|
||||
"$ruExtension.$ExtensionName.$parentKindRu.$parentName.$ruForm.$artifactName"
|
||||
}
|
||||
elseif ($parentKind) {
|
||||
"$ruExtension.$ExtensionName.$parentKindRu.$parentName.$artifactKindRu.$artifactName"
|
||||
}
|
||||
else {
|
||||
"$ruExtension.$ExtensionName.$artifactKindRu.$artifactName"
|
||||
}
|
||||
return [pscustomobject]@{
|
||||
layer = "extension"
|
||||
extension = $ExtensionName
|
||||
parent_kind = $parentKind
|
||||
parent_kind_ru = $parentKindRu
|
||||
parent_name = $parentName
|
||||
kind = $artifactKind
|
||||
kind_ru = $artifactKindRu
|
||||
name = $artifactName
|
||||
synonym = $synonym
|
||||
full_name = $fullName
|
||||
guid = $ObjectGuid
|
||||
relative_path = $relativePath
|
||||
evidence = [pscustomobject]@{
|
||||
xml_path = if ($top) { [string]$top.path } else { $null }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Split-ObjectPart {
|
||||
param([string]$FileName)
|
||||
if ($FileName -match '^([0-9a-fA-F-]{36})(\..+)?$') {
|
||||
return [pscustomobject]@{ object_id = $Matches[1].ToLowerInvariant(); suffix = if ($Matches[2]) { $Matches[2] } else { "" } }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Split-ExtensionFileName {
|
||||
param([string]$FileName)
|
||||
if ($FileName -match '^([0-9a-fA-F-]{36})__(.+)$') {
|
||||
$rest = $Matches[2]
|
||||
if ($rest -eq "configinfo") {
|
||||
return [pscustomobject]@{ extension_id = $Matches[1].ToLowerInvariant(); object_id = $null; suffix = ""; system_name = "configinfo" }
|
||||
}
|
||||
$part = Split-ObjectPart $rest
|
||||
return [pscustomobject]@{ extension_id = $Matches[1].ToLowerInvariant(); object_id = $part.object_id; suffix = $part.suffix; system_name = $null }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function New-StorageEvidence {
|
||||
param(
|
||||
[string]$SavedTable,
|
||||
[string]$ActiveTable,
|
||||
[string]$FileName,
|
||||
[object]$Saved,
|
||||
[object]$Active
|
||||
)
|
||||
return [pscustomobject]@{
|
||||
saved_table = $SavedTable
|
||||
active_table = $ActiveTable
|
||||
file_name = $FileName
|
||||
saved_bytes = $Saved.bytes
|
||||
active_bytes = if ($Active) { $Active.bytes } else { $null }
|
||||
saved_sha256 = $Saved.sha256
|
||||
active_sha256 = if ($Active) { $Active.sha256 } else { $null }
|
||||
active_exists = [bool]$Active
|
||||
changed = (-not $Active) -or ($Saved.sha256 -ne $Active.sha256) -or ($Saved.bytes -ne $Active.bytes)
|
||||
}
|
||||
}
|
||||
|
||||
$connection = New-Connection
|
||||
try {
|
||||
$configSave = Read-TableFiles $connection "ConfigSave" $null
|
||||
$configCassave = Read-TableFiles $connection "ConfigCASSave" $null
|
||||
$configActive = Read-TableFiles $connection "Config" @($configSave.Keys)
|
||||
$configCasActive = Read-TableFiles $connection "ConfigCAS" @($configCassave.Keys)
|
||||
$extensions = Read-Extensions $connection
|
||||
}
|
||||
finally {
|
||||
if ($connection -ne $null) { $connection.Dispose() }
|
||||
}
|
||||
|
||||
$objectChangesByKey = @{}
|
||||
$systemChanges = @()
|
||||
|
||||
foreach ($fileName in $configSave.Keys) {
|
||||
$saved = $configSave[$fileName]
|
||||
$active = if ($configActive.ContainsKey($fileName)) { $configActive[$fileName] } else { $null }
|
||||
$evidence = New-StorageEvidence "ConfigSave" "Config" $fileName $saved $active
|
||||
if (-not $evidence.changed) { continue }
|
||||
$part = Split-ObjectPart $fileName
|
||||
if (-not $part) {
|
||||
$systemChanges += [pscustomobject]@{ layer = "base"; name = $fileName; storage = $evidence }
|
||||
continue
|
||||
}
|
||||
$info = Get-BaseObjectInfo $part.object_id
|
||||
if (-not $info) {
|
||||
$info = [pscustomobject]@{
|
||||
layer = "base"
|
||||
kind = "ConfigObject"
|
||||
kind_ru = $ruConfigObject
|
||||
name = $part.object_id
|
||||
synonym = $null
|
||||
full_name = "$ruConfigObject.$($part.object_id)"
|
||||
guid = $part.object_id
|
||||
evidence = $null
|
||||
}
|
||||
}
|
||||
$key = "base::$($part.object_id)"
|
||||
if (-not $objectChangesByKey.ContainsKey($key)) {
|
||||
$objectChangesByKey[$key] = [ordered]@{
|
||||
layer = $info.layer
|
||||
kind = $info.kind
|
||||
kind_ru = $info.kind_ru
|
||||
name = $info.name
|
||||
synonym = $info.synonym
|
||||
full_name = $info.full_name
|
||||
guid = $info.guid
|
||||
change_state = "saved_not_applied"
|
||||
storage = @()
|
||||
evidence = $info.evidence
|
||||
}
|
||||
}
|
||||
$objectChangesByKey[$key].storage += $evidence
|
||||
}
|
||||
|
||||
$extensionGuidMap = Get-ExtensionGuidMap
|
||||
foreach ($fileName in $configCassave.Keys) {
|
||||
$saved = $configCassave[$fileName]
|
||||
$active = if ($configCasActive.ContainsKey($fileName)) { $configCasActive[$fileName] } else { $null }
|
||||
$evidence = New-StorageEvidence "ConfigCASSave" "ConfigCAS" $fileName $saved $active
|
||||
if (-not $evidence.changed) { continue }
|
||||
$part = Split-ExtensionFileName $fileName
|
||||
if (-not $part) {
|
||||
$systemChanges += [pscustomobject]@{ layer = "extension"; name = $fileName; storage = $evidence }
|
||||
continue
|
||||
}
|
||||
$extension = if ($extensions.ContainsKey($part.extension_id)) { $extensions[$part.extension_id] } else { $null }
|
||||
$extensionName = if ($extension) { $extension.name } else { $part.extension_id }
|
||||
if ($part.system_name) {
|
||||
$systemChanges += [pscustomobject]@{ layer = "extension"; extension = $extensionName; name = $part.system_name; storage = $evidence }
|
||||
continue
|
||||
}
|
||||
$info = Get-ExtensionObjectInfo $extensionName $part.object_id $extensionGuidMap
|
||||
$key = "extension::$($part.extension_id)::$($part.object_id)"
|
||||
if (-not $objectChangesByKey.ContainsKey($key)) {
|
||||
$objectChangesByKey[$key] = [ordered]@{
|
||||
layer = $info.layer
|
||||
extension = $info.extension
|
||||
parent_kind = $info.parent_kind
|
||||
parent_kind_ru = $info.parent_kind_ru
|
||||
parent_name = $info.parent_name
|
||||
kind = $info.kind
|
||||
kind_ru = $info.kind_ru
|
||||
name = $info.name
|
||||
synonym = $info.synonym
|
||||
full_name = $info.full_name
|
||||
guid = $info.guid
|
||||
relative_path = $info.relative_path
|
||||
change_state = "saved_not_applied"
|
||||
storage = @()
|
||||
evidence = $info.evidence
|
||||
}
|
||||
}
|
||||
$objectChangesByKey[$key].storage += $evidence
|
||||
}
|
||||
|
||||
$objectChanges = @($objectChangesByKey.Values | ForEach-Object { [pscustomobject]$_ } | Sort-Object layer, extension, full_name)
|
||||
$result = [pscustomobject]@{
|
||||
schema = "onec_saved_state_object_comparison.v1"
|
||||
server = $Server
|
||||
database = $Database
|
||||
view = "saved_not_applied_vs_active"
|
||||
object_changes = $objectChanges
|
||||
system_changes = $systemChanges
|
||||
counts = [pscustomobject]@{
|
||||
object_changes = $objectChanges.Count
|
||||
system_changes = $systemChanges.Count
|
||||
config_save_files = $configSave.Count
|
||||
config_cas_save_files = $configCassave.Count
|
||||
}
|
||||
safety = [pscustomobject]@{
|
||||
read_only = $true
|
||||
sql_write_performed = $false
|
||||
exposes_storage_evidence = $true
|
||||
public_terms_are_1c_objects = $true
|
||||
}
|
||||
}
|
||||
|
||||
$json = $result | ConvertTo-Json -Depth 20
|
||||
if ($Output) {
|
||||
$parent = Split-Path -Parent $Output
|
||||
if ($parent) { New-Item -ItemType Directory -Force -Path $parent | Out-Null }
|
||||
$json | Set-Content -LiteralPath $Output -Encoding UTF8
|
||||
}
|
||||
$json
|
||||
Reference in New Issue
Block a user