mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-26 15:04:34 +03:00
Auto-build: codeassistant (powershell) from cbde49e
This commit is contained in:
@@ -0,0 +1,616 @@
|
||||
# meta-remove v1.3 — Remove metadata object from 1C configuration dump
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$ConfigDir,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Object,
|
||||
|
||||
[switch]$DryRun,
|
||||
|
||||
[switch]$KeepFiles,
|
||||
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Type → plural directory mapping ---
|
||||
|
||||
$typePluralMap = @{
|
||||
"Catalog" = "Catalogs"
|
||||
"Document" = "Documents"
|
||||
"Enum" = "Enums"
|
||||
"Constant" = "Constants"
|
||||
"InformationRegister" = "InformationRegisters"
|
||||
"AccumulationRegister" = "AccumulationRegisters"
|
||||
"AccountingRegister" = "AccountingRegisters"
|
||||
"CalculationRegister" = "CalculationRegisters"
|
||||
"ChartOfAccounts" = "ChartsOfAccounts"
|
||||
"ChartOfCharacteristicTypes" = "ChartsOfCharacteristicTypes"
|
||||
"ChartOfCalculationTypes" = "ChartsOfCalculationTypes"
|
||||
"BusinessProcess" = "BusinessProcesses"
|
||||
"Task" = "Tasks"
|
||||
"ExchangePlan" = "ExchangePlans"
|
||||
"DocumentJournal" = "DocumentJournals"
|
||||
"Report" = "Reports"
|
||||
"DataProcessor" = "DataProcessors"
|
||||
"CommonModule" = "CommonModules"
|
||||
"ScheduledJob" = "ScheduledJobs"
|
||||
"EventSubscription" = "EventSubscriptions"
|
||||
"HTTPService" = "HTTPServices"
|
||||
"WebService" = "WebServices"
|
||||
"DefinedType" = "DefinedTypes"
|
||||
"Role" = "Roles"
|
||||
"Subsystem" = "Subsystems"
|
||||
"CommonForm" = "CommonForms"
|
||||
"CommonTemplate" = "CommonTemplates"
|
||||
"CommonPicture" = "CommonPictures"
|
||||
"CommonAttribute" = "CommonAttributes"
|
||||
"SessionParameter" = "SessionParameters"
|
||||
"FunctionalOption" = "FunctionalOptions"
|
||||
"FunctionalOptionsParameter" = "FunctionalOptionsParameters"
|
||||
"Sequence" = "Sequences"
|
||||
"FilterCriterion" = "FilterCriteria"
|
||||
"SettingsStorage" = "SettingsStorages"
|
||||
"XDTOPackage" = "XDTOPackages"
|
||||
"WSReference" = "WSReferences"
|
||||
"StyleItem" = "StyleItems"
|
||||
"Language" = "Languages"
|
||||
}
|
||||
|
||||
# --- Resolve paths ---
|
||||
|
||||
if (-not [System.IO.Path]::IsPathRooted($ConfigDir)) {
|
||||
$ConfigDir = Join-Path (Get-Location).Path $ConfigDir
|
||||
}
|
||||
|
||||
if (-not (Test-Path $ConfigDir -PathType Container)) {
|
||||
Write-Host "[ERROR] Config directory not found: $ConfigDir"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$configXml = Join-Path $ConfigDir "Configuration.xml"
|
||||
if (-not (Test-Path $configXml)) {
|
||||
Write-Host "[ERROR] Configuration.xml not found in: $ConfigDir"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
# --- Parse object spec ---
|
||||
|
||||
$parts = $Object -split "\.", 2
|
||||
if ($parts.Count -ne 2 -or -not $parts[0] -or -not $parts[1]) {
|
||||
Write-Host "[ERROR] Invalid object format '$Object'. Expected: Type.Name (e.g. Catalog.Товары)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$objType = $parts[0]
|
||||
$objName = $parts[1]
|
||||
|
||||
if (-not $typePluralMap.ContainsKey($objType)) {
|
||||
Write-Host "[ERROR] Unknown type '$objType'. Supported: $($typePluralMap.Keys -join ', ')"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$typePlural = $typePluralMap[$objType]
|
||||
|
||||
Write-Host "=== meta-remove: ${objType}.${objName} ==="
|
||||
Write-Host ""
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "[DRY-RUN] No changes will be made"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
$actions = 0
|
||||
$errors = 0
|
||||
|
||||
# --- 1. Find object files ---
|
||||
|
||||
$typeDir = Join-Path $ConfigDir $typePlural
|
||||
$objXml = Join-Path $typeDir "$objName.xml"
|
||||
$objDir = Join-Path $typeDir $objName
|
||||
|
||||
# Support guard — removal requires the object be снят-с-поддержки (f1=2).
|
||||
Assert-EditAllowed $objXml 'removed'
|
||||
|
||||
$hasXml = Test-Path $objXml
|
||||
$hasDir = Test-Path $objDir -PathType Container
|
||||
|
||||
if (-not $hasXml -and -not $hasDir) {
|
||||
# Check if registered in Configuration.xml before proceeding
|
||||
$cfgCheckDoc = New-Object System.Xml.XmlDocument
|
||||
$cfgCheckDoc.PreserveWhitespace = $true
|
||||
$cfgCheckDoc.Load($configXml)
|
||||
$cfgCheckNs = New-Object System.Xml.XmlNamespaceManager($cfgCheckDoc.NameTable)
|
||||
$cfgCheckNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$cfgCheckNode = $cfgCheckDoc.DocumentElement.SelectSingleNode("md:Configuration/md:ChildObjects", $cfgCheckNs)
|
||||
$registeredInCfg = $false
|
||||
if ($cfgCheckNode) {
|
||||
foreach ($child in @($cfgCheckNode.ChildNodes)) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -eq $objType -and $child.InnerText.Trim() -eq $objName) {
|
||||
$registeredInCfg = $true; break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $registeredInCfg) {
|
||||
Write-Host "[ERROR] Object not found: $typePlural/$objName.xml and not registered in Configuration.xml"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[WARN] Object files not found: $typePlural/$objName.xml"
|
||||
Write-Host " Proceeding with deregistration only..."
|
||||
} else {
|
||||
if ($hasXml) { Write-Host "[FOUND] $typePlural/$objName.xml" }
|
||||
if ($hasDir) {
|
||||
$fileCount = @(Get-ChildItem $objDir -Recurse -File).Count
|
||||
Write-Host "[FOUND] $typePlural/$objName/ ($fileCount files)"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 2. Reference check ---
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "--- Reference check ---"
|
||||
|
||||
# Build search patterns based on object type
|
||||
|
||||
# Type → reference type name (used in XML <v8:Type> elements)
|
||||
$typeRefNames = @{
|
||||
"Catalog" = @("CatalogRef","CatalogObject")
|
||||
"Document" = @("DocumentRef","DocumentObject")
|
||||
"Enum" = @("EnumRef")
|
||||
"ExchangePlan" = @("ExchangePlanRef","ExchangePlanObject")
|
||||
"ChartOfAccounts" = @("ChartOfAccountsRef","ChartOfAccountsObject")
|
||||
"ChartOfCharacteristicTypes" = @("ChartOfCharacteristicTypesRef","ChartOfCharacteristicTypesObject")
|
||||
"ChartOfCalculationTypes" = @("ChartOfCalculationTypesRef","ChartOfCalculationTypesObject")
|
||||
"BusinessProcess" = @("BusinessProcessRef","BusinessProcessObject")
|
||||
"Task" = @("TaskRef","TaskObject")
|
||||
}
|
||||
|
||||
# Type → Russian manager name (used in BSL code: Справочники.Товары)
|
||||
$typeRuManager = @{
|
||||
"Catalog" = "Справочники"
|
||||
"Document" = "Документы"
|
||||
"Enum" = "Перечисления"
|
||||
"Constant" = "Константы"
|
||||
"InformationRegister" = "РегистрыСведений"
|
||||
"AccumulationRegister" = "РегистрыНакопления"
|
||||
"AccountingRegister" = "РегистрыБухгалтерии"
|
||||
"CalculationRegister" = "РегистрыРасчета"
|
||||
"ChartOfAccounts" = "ПланыСчетов"
|
||||
"ChartOfCharacteristicTypes" = "ПланыВидовХарактеристик"
|
||||
"ChartOfCalculationTypes" = "ПланыВидовРасчета"
|
||||
"BusinessProcess" = "БизнесПроцессы"
|
||||
"Task" = "Задачи"
|
||||
"ExchangePlan" = "ПланыОбмена"
|
||||
"Report" = "Отчеты"
|
||||
"DataProcessor" = "Обработки"
|
||||
"DocumentJournal" = "ЖурналыДокументов"
|
||||
"CommonModule" = $null
|
||||
}
|
||||
|
||||
$searchPatterns = @()
|
||||
|
||||
# 1) XML type references: CatalogRef.Name, CatalogObject.Name
|
||||
if ($typeRefNames.ContainsKey($objType)) {
|
||||
foreach ($refName in $typeRefNames[$objType]) {
|
||||
$searchPatterns += "$refName.$objName"
|
||||
}
|
||||
}
|
||||
|
||||
# 2) BSL code references: Справочники.Name, Catalogs.Name
|
||||
$ruMgr = $typeRuManager[$objType]
|
||||
if ($ruMgr) {
|
||||
$searchPatterns += "$ruMgr.$objName"
|
||||
}
|
||||
# English manager = plural directory name
|
||||
$searchPatterns += "$typePlural.$objName"
|
||||
|
||||
# 3) CommonModule: method calls in BSL (ModuleName.)
|
||||
if ($objType -eq "CommonModule") {
|
||||
$searchPatterns += "$objName."
|
||||
}
|
||||
|
||||
# 4) ScheduledJob/EventSubscription handler references
|
||||
if ($objType -eq "CommonModule") {
|
||||
$searchPatterns += "<Handler>$objName."
|
||||
$searchPatterns += "<MethodName>$objName."
|
||||
}
|
||||
|
||||
# Exclude object's own files from search
|
||||
$excludeDirs = @()
|
||||
if ($hasDir) { $excludeDirs += $objDir }
|
||||
$excludeFile = ""
|
||||
if ($hasXml) { $excludeFile = $objXml }
|
||||
|
||||
# Search all XML and BSL files
|
||||
$references = @()
|
||||
$searchExtensions = @("*.xml", "*.bsl")
|
||||
|
||||
foreach ($ext in $searchExtensions) {
|
||||
$files = @(Get-ChildItem $ConfigDir -Filter $ext -Recurse -File -ErrorAction SilentlyContinue)
|
||||
foreach ($file in $files) {
|
||||
# Skip own files
|
||||
if ($excludeFile -and $file.FullName -eq $excludeFile) { continue }
|
||||
if ($excludeDirs.Count -gt 0) {
|
||||
$skip = $false
|
||||
foreach ($ed in $excludeDirs) {
|
||||
if ($file.FullName.StartsWith($ed)) { $skip = $true; break }
|
||||
}
|
||||
if ($skip) { continue }
|
||||
}
|
||||
# Skip auto-cleaned files (Configuration.xml, ConfigDumpInfo.xml, Subsystems)
|
||||
$relPath = $file.FullName.Substring($ConfigDir.Length + 1)
|
||||
if ($relPath -eq "Configuration.xml" -or $relPath -eq "ConfigDumpInfo.xml" -or $relPath.StartsWith("Subsystems")) { continue }
|
||||
|
||||
$content = [System.IO.File]::ReadAllText($file.FullName, [System.Text.Encoding]::UTF8)
|
||||
foreach ($pat in $searchPatterns) {
|
||||
if ($content.Contains($pat)) {
|
||||
$references += @{ File = $relPath; Pattern = $pat }
|
||||
break # one match per file is enough
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Also check for Type.Name references (subsystem content, doc journal, etc.) — but NOT in own files
|
||||
$typeNameRef = "${objType}.${objName}"
|
||||
$files = @(Get-ChildItem $ConfigDir -Filter "*.xml" -Recurse -File -ErrorAction SilentlyContinue)
|
||||
foreach ($file in $files) {
|
||||
if ($excludeFile -and $file.FullName -eq $excludeFile) { continue }
|
||||
if ($excludeDirs.Count -gt 0) {
|
||||
$skip = $false
|
||||
foreach ($ed in $excludeDirs) {
|
||||
if ($file.FullName.StartsWith($ed)) { $skip = $true; break }
|
||||
}
|
||||
if ($skip) { continue }
|
||||
}
|
||||
# Skip Configuration.xml and Subsystems — they will be cleaned automatically
|
||||
$relPath = $file.FullName.Substring($ConfigDir.Length + 1)
|
||||
if ($relPath -eq "Configuration.xml") { continue }
|
||||
if ($relPath -eq "ConfigDumpInfo.xml") { continue }
|
||||
if ($relPath.StartsWith("Subsystems")) { continue }
|
||||
|
||||
$content = [System.IO.File]::ReadAllText($file.FullName, [System.Text.Encoding]::UTF8)
|
||||
if ($content.Contains($typeNameRef)) {
|
||||
# Check it's not already in references
|
||||
$alreadyFound = $false
|
||||
foreach ($r in $references) {
|
||||
if ($r.File -eq $relPath) { $alreadyFound = $true; break }
|
||||
}
|
||||
if (-not $alreadyFound) {
|
||||
$references += @{ File = $relPath; Pattern = $typeNameRef }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($references.Count -gt 0) {
|
||||
Write-Host "[WARN] Found $($references.Count) reference(s) to ${objType}.${objName}:"
|
||||
Write-Host ""
|
||||
$shown = 0
|
||||
foreach ($ref in $references) {
|
||||
Write-Host " $($ref.File)"
|
||||
Write-Host " pattern: $($ref.Pattern)"
|
||||
$shown++
|
||||
if ($shown -ge 20) {
|
||||
$remaining = $references.Count - $shown
|
||||
if ($remaining -gt 0) {
|
||||
Write-Host " ... and $remaining more"
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
if (-not $Force) {
|
||||
Write-Host "[ERROR] Cannot remove: object has $($references.Count) reference(s)."
|
||||
Write-Host " Use -Force to remove anyway, or fix references first."
|
||||
exit 1
|
||||
} else {
|
||||
Write-Host "[WARN] -Force specified, proceeding despite references"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[OK] No references found"
|
||||
}
|
||||
|
||||
# --- 3. Remove from Configuration.xml ChildObjects ---
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "--- Configuration.xml ---"
|
||||
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$xmlDoc.PreserveWhitespace = $true
|
||||
$xmlDoc.Load($configXml)
|
||||
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||
|
||||
$cfgNode = $xmlDoc.DocumentElement.SelectSingleNode("md:Configuration", $ns)
|
||||
if (-not $cfgNode) {
|
||||
Write-Host "[ERROR] Configuration element not found in Configuration.xml"
|
||||
$errors++
|
||||
} else {
|
||||
$childObjects = $cfgNode.SelectSingleNode("md:ChildObjects", $ns)
|
||||
if ($childObjects) {
|
||||
$found = $false
|
||||
foreach ($child in @($childObjects.ChildNodes)) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -eq $objType -and $child.InnerText.Trim() -eq $objName) {
|
||||
$found = $true
|
||||
if (-not $DryRun) {
|
||||
# Remove preceding whitespace if present
|
||||
$prev = $child.PreviousSibling
|
||||
if ($prev -and $prev.NodeType -eq 'Whitespace') {
|
||||
$childObjects.RemoveChild($prev) | Out-Null
|
||||
}
|
||||
$childObjects.RemoveChild($child) | Out-Null
|
||||
}
|
||||
Write-Host "[OK] Removed <$objType>$objName</$objType> from ChildObjects"
|
||||
$actions++
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $found) {
|
||||
Write-Host "[WARN] <$objType>$objName</$objType> not found in ChildObjects"
|
||||
}
|
||||
}
|
||||
|
||||
# Save Configuration.xml
|
||||
if ($actions -gt 0 -and -not $DryRun) {
|
||||
$enc = New-Object System.Text.UTF8Encoding $true
|
||||
$sw = New-Object System.IO.StreamWriter($configXml, $false, $enc)
|
||||
$xmlDoc.Save($sw)
|
||||
$sw.Close()
|
||||
Write-Host "[OK] Configuration.xml saved"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 4. Remove from subsystem Content ---
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "--- Subsystems ---"
|
||||
|
||||
$subsystemsDir = Join-Path $ConfigDir "Subsystems"
|
||||
$subsystemsFound = 0
|
||||
$subsystemsCleaned = 0
|
||||
|
||||
function Remove-FromSubsystems {
|
||||
param([string]$dir)
|
||||
|
||||
$xmlFiles = @(Get-ChildItem $dir -Filter "*.xml" -File -ErrorAction SilentlyContinue)
|
||||
foreach ($xmlFile in $xmlFiles) {
|
||||
$ssDoc = New-Object System.Xml.XmlDocument
|
||||
$ssDoc.PreserveWhitespace = $true
|
||||
try { $ssDoc.Load($xmlFile.FullName) } catch { continue }
|
||||
|
||||
$ssNs = New-Object System.Xml.XmlNamespaceManager($ssDoc.NameTable)
|
||||
$ssNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ssNs.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||
|
||||
$ssNode = $ssDoc.DocumentElement.SelectSingleNode("md:Subsystem", $ssNs)
|
||||
if (-not $ssNode) { continue }
|
||||
|
||||
$propsNode = $ssNode.SelectSingleNode("md:Properties", $ssNs)
|
||||
if (-not $propsNode) { continue }
|
||||
|
||||
$contentNode = $propsNode.SelectSingleNode("md:Content", $ssNs)
|
||||
if (-not $contentNode) { continue }
|
||||
|
||||
$ssNameNode = $propsNode.SelectSingleNode("md:Name", $ssNs)
|
||||
$ssName = if ($ssNameNode) { $ssNameNode.InnerText } else { $xmlFile.BaseName }
|
||||
|
||||
# Content items are <v8:Value>Type.Name</v8:Value>
|
||||
$targetRef = "${objType}.${objName}"
|
||||
$modified = $false
|
||||
|
||||
foreach ($item in @($contentNode.ChildNodes)) {
|
||||
if ($item.NodeType -ne 'Element') { continue }
|
||||
$val = $item.InnerText.Trim()
|
||||
# Content format: "Subsystem.X" or "Catalog.X" etc.
|
||||
if ($val -eq $targetRef) {
|
||||
$script:subsystemsFound++
|
||||
if (-not $DryRun) {
|
||||
$prev = $item.PreviousSibling
|
||||
if ($prev -and $prev.NodeType -eq 'Whitespace') {
|
||||
$contentNode.RemoveChild($prev) | Out-Null
|
||||
}
|
||||
$contentNode.RemoveChild($item) | Out-Null
|
||||
$modified = $true
|
||||
}
|
||||
Write-Host "[OK] Removed from subsystem '$ssName'"
|
||||
$script:subsystemsCleaned++
|
||||
}
|
||||
}
|
||||
|
||||
if ($modified -and -not $DryRun) {
|
||||
$enc = New-Object System.Text.UTF8Encoding $true
|
||||
$sw = New-Object System.IO.StreamWriter($xmlFile.FullName, $false, $enc)
|
||||
$ssDoc.Save($sw)
|
||||
$sw.Close()
|
||||
}
|
||||
|
||||
# Recurse into child subsystems
|
||||
$childDir = Join-Path $dir ($xmlFile.BaseName)
|
||||
$childSubsystems = Join-Path $childDir "Subsystems"
|
||||
if (Test-Path $childSubsystems -PathType Container) {
|
||||
Remove-FromSubsystems -dir $childSubsystems
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path $subsystemsDir -PathType Container) {
|
||||
Remove-FromSubsystems -dir $subsystemsDir
|
||||
if ($subsystemsCleaned -eq 0) {
|
||||
Write-Host "[OK] Not referenced in any subsystem"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[OK] No Subsystems directory"
|
||||
}
|
||||
|
||||
# --- 5. Delete object files ---
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "--- Files ---"
|
||||
|
||||
if (-not $KeepFiles) {
|
||||
if ($hasDir -and -not $DryRun) {
|
||||
Remove-Item $objDir -Recurse -Force
|
||||
Write-Host "[OK] Deleted directory: $typePlural/$objName/"
|
||||
$actions++
|
||||
} elseif ($hasDir) {
|
||||
Write-Host "[DRY] Would delete directory: $typePlural/$objName/"
|
||||
$actions++
|
||||
}
|
||||
|
||||
if ($hasXml -and -not $DryRun) {
|
||||
Remove-Item $objXml -Force
|
||||
Write-Host "[OK] Deleted file: $typePlural/$objName.xml"
|
||||
$actions++
|
||||
} elseif ($hasXml) {
|
||||
Write-Host "[DRY] Would delete file: $typePlural/$objName.xml"
|
||||
$actions++
|
||||
}
|
||||
|
||||
if (-not $hasXml -and -not $hasDir) {
|
||||
Write-Host "[OK] No files to delete"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[SKIP] File deletion skipped (-KeepFiles)"
|
||||
}
|
||||
|
||||
# --- Summary ---
|
||||
|
||||
Write-Host ""
|
||||
$totalActions = $actions + $subsystemsCleaned
|
||||
if ($DryRun) {
|
||||
Write-Host "=== Dry run complete: $totalActions actions would be performed ==="
|
||||
} else {
|
||||
Write-Host "=== Done: $totalActions actions performed ($subsystemsCleaned subsystem references removed) ==="
|
||||
}
|
||||
|
||||
if ($errors -gt 0) {
|
||||
exit 1
|
||||
}
|
||||
exit 0
|
||||
@@ -0,0 +1,649 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-remove v1.3 — Remove metadata object from 1C configuration dump
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import shutil
|
||||
from lxml import etree
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
# --- Type -> plural directory mapping ---
|
||||
|
||||
TYPE_PLURAL_MAP = {
|
||||
"Catalog": "Catalogs",
|
||||
"Document": "Documents",
|
||||
"Enum": "Enums",
|
||||
"Constant": "Constants",
|
||||
"InformationRegister": "InformationRegisters",
|
||||
"AccumulationRegister": "AccumulationRegisters",
|
||||
"AccountingRegister": "AccountingRegisters",
|
||||
"CalculationRegister": "CalculationRegisters",
|
||||
"ChartOfAccounts": "ChartsOfAccounts",
|
||||
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
||||
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
|
||||
"BusinessProcess": "BusinessProcesses",
|
||||
"Task": "Tasks",
|
||||
"ExchangePlan": "ExchangePlans",
|
||||
"DocumentJournal": "DocumentJournals",
|
||||
"Report": "Reports",
|
||||
"DataProcessor": "DataProcessors",
|
||||
"CommonModule": "CommonModules",
|
||||
"ScheduledJob": "ScheduledJobs",
|
||||
"EventSubscription": "EventSubscriptions",
|
||||
"HTTPService": "HTTPServices",
|
||||
"WebService": "WebServices",
|
||||
"DefinedType": "DefinedTypes",
|
||||
"Role": "Roles",
|
||||
"Subsystem": "Subsystems",
|
||||
"CommonForm": "CommonForms",
|
||||
"CommonTemplate": "CommonTemplates",
|
||||
"CommonPicture": "CommonPictures",
|
||||
"CommonAttribute": "CommonAttributes",
|
||||
"SessionParameter": "SessionParameters",
|
||||
"FunctionalOption": "FunctionalOptions",
|
||||
"FunctionalOptionsParameter": "FunctionalOptionsParameters",
|
||||
"Sequence": "Sequences",
|
||||
"FilterCriterion": "FilterCriteria",
|
||||
"SettingsStorage": "SettingsStorages",
|
||||
"XDTOPackage": "XDTOPackages",
|
||||
"WSReference": "WSReferences",
|
||||
"StyleItem": "StyleItems",
|
||||
"Language": "Languages",
|
||||
}
|
||||
|
||||
# Type -> reference type names (used in XML <v8:Type> elements)
|
||||
TYPE_REF_NAMES = {
|
||||
"Catalog": ["CatalogRef", "CatalogObject"],
|
||||
"Document": ["DocumentRef", "DocumentObject"],
|
||||
"Enum": ["EnumRef"],
|
||||
"ExchangePlan": ["ExchangePlanRef", "ExchangePlanObject"],
|
||||
"ChartOfAccounts": ["ChartOfAccountsRef", "ChartOfAccountsObject"],
|
||||
"ChartOfCharacteristicTypes": ["ChartOfCharacteristicTypesRef", "ChartOfCharacteristicTypesObject"],
|
||||
"ChartOfCalculationTypes": ["ChartOfCalculationTypesRef", "ChartOfCalculationTypesObject"],
|
||||
"BusinessProcess": ["BusinessProcessRef", "BusinessProcessObject"],
|
||||
"Task": ["TaskRef", "TaskObject"],
|
||||
}
|
||||
|
||||
# Type -> Russian manager name (used in BSL code)
|
||||
TYPE_RU_MANAGER = {
|
||||
"Catalog": "\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u0438",
|
||||
"Document": "\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b",
|
||||
"Enum": "\u041f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f",
|
||||
"Constant": "\u041a\u043e\u043d\u0441\u0442\u0430\u043d\u0442\u044b",
|
||||
"InformationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0421\u0432\u0435\u0434\u0435\u043d\u0438\u0439",
|
||||
"AccumulationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u041d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f",
|
||||
"AccountingRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0411\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0438",
|
||||
"CalculationRegister": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u044b\u0420\u0430\u0441\u0447\u0435\u0442\u0430",
|
||||
"ChartOfAccounts": "\u041f\u043b\u0430\u043d\u044b\u0421\u0447\u0435\u0442\u043e\u0432",
|
||||
"ChartOfCharacteristicTypes": "\u041f\u043b\u0430\u043d\u044b\u0412\u0438\u0434\u043e\u0432\u0425\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a",
|
||||
"ChartOfCalculationTypes": "\u041f\u043b\u0430\u043d\u044b\u0412\u0438\u0434\u043e\u0432\u0420\u0430\u0441\u0447\u0435\u0442\u0430",
|
||||
"BusinessProcess": "\u0411\u0438\u0437\u043d\u0435\u0441\u041f\u0440\u043e\u0446\u0435\u0441\u0441\u044b",
|
||||
"Task": "\u0417\u0430\u0434\u0430\u0447\u0438",
|
||||
"ExchangePlan": "\u041f\u043b\u0430\u043d\u044b\u041e\u0431\u043c\u0435\u043d\u0430",
|
||||
"Report": "\u041e\u0442\u0447\u0435\u0442\u044b",
|
||||
"DataProcessor": "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438",
|
||||
"DocumentJournal": "\u0416\u0443\u0440\u043d\u0430\u043b\u044b\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432",
|
||||
"CommonModule": None,
|
||||
}
|
||||
|
||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||
|
||||
NSMAP = {"md": MD_NS, "v8": V8_NS}
|
||||
|
||||
|
||||
def localname(el):
|
||||
return etree.QName(el.tag).localname
|
||||
|
||||
|
||||
def save_xml_bom(tree, path):
|
||||
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
if not xml_bytes.endswith(b"\n"):
|
||||
xml_bytes += b"\n"
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"\xef\xbb\xbf")
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(description="Remove metadata object from 1C configuration dump", allow_abbrev=False)
|
||||
parser.add_argument("-ConfigDir", required=True)
|
||||
parser.add_argument("-Object", required=True)
|
||||
parser.add_argument("-DryRun", action="store_true")
|
||||
parser.add_argument("-KeepFiles", action="store_true")
|
||||
parser.add_argument("-Force", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
config_dir = args.ConfigDir
|
||||
if not os.path.isabs(config_dir):
|
||||
config_dir = os.path.join(os.getcwd(), config_dir)
|
||||
|
||||
if not os.path.isdir(config_dir):
|
||||
print(f"[ERROR] Config directory not found: {config_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
config_xml = os.path.join(config_dir, "Configuration.xml")
|
||||
if not os.path.isfile(config_xml):
|
||||
print(f"[ERROR] Configuration.xml not found in: {config_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Parse object spec ---
|
||||
parts = args.Object.split(".", 1)
|
||||
if len(parts) != 2 or not parts[0] or not parts[1]:
|
||||
print(f"[ERROR] Invalid object format '{args.Object}'. Expected: Type.Name (e.g. Catalog.\u0422\u043e\u0432\u0430\u0440\u044b)")
|
||||
sys.exit(1)
|
||||
|
||||
obj_type = parts[0]
|
||||
obj_name = parts[1]
|
||||
|
||||
if obj_type not in TYPE_PLURAL_MAP:
|
||||
print(f"[ERROR] Unknown type '{obj_type}'. Supported: {', '.join(TYPE_PLURAL_MAP.keys())}")
|
||||
sys.exit(1)
|
||||
|
||||
type_plural = TYPE_PLURAL_MAP[obj_type]
|
||||
|
||||
print(f"=== meta-remove: {obj_type}.{obj_name} ===")
|
||||
print()
|
||||
|
||||
if args.DryRun:
|
||||
print("[DRY-RUN] No changes will be made")
|
||||
print()
|
||||
|
||||
actions = 0
|
||||
errors = 0
|
||||
|
||||
# --- 1. Find object files ---
|
||||
type_dir = os.path.join(config_dir, type_plural)
|
||||
obj_xml = os.path.join(type_dir, f"{obj_name}.xml")
|
||||
obj_dir = os.path.join(type_dir, obj_name)
|
||||
|
||||
# Support guard — removal requires the object be снят-с-поддержки (f1=2).
|
||||
assert_edit_allowed(obj_xml, "removed")
|
||||
|
||||
has_xml = os.path.isfile(obj_xml)
|
||||
has_dir = os.path.isdir(obj_dir)
|
||||
|
||||
if not has_xml and not has_dir:
|
||||
# Check if registered in Configuration.xml before proceeding
|
||||
cfg_check_tree = etree.parse(config_xml, etree.XMLParser(remove_blank_text=False))
|
||||
cfg_check_root = cfg_check_tree.getroot()
|
||||
child_objects = cfg_check_root.find(f"{{{MD_NS}}}Configuration/{{{MD_NS}}}ChildObjects")
|
||||
registered_in_cfg = False
|
||||
if child_objects is not None:
|
||||
for child in child_objects:
|
||||
if isinstance(child.tag, str) and etree.QName(child.tag).localname == obj_type and (child.text or "").strip() == obj_name:
|
||||
registered_in_cfg = True
|
||||
break
|
||||
if not registered_in_cfg:
|
||||
print(f"[ERROR] Object not found: {type_plural}/{obj_name}.xml and not registered in Configuration.xml")
|
||||
sys.exit(1)
|
||||
print(f"[WARN] Object files not found: {type_plural}/{obj_name}.xml")
|
||||
print(" Proceeding with deregistration only...")
|
||||
else:
|
||||
if has_xml:
|
||||
print(f"[FOUND] {type_plural}/{obj_name}.xml")
|
||||
if has_dir:
|
||||
file_count = sum(len(files) for _, _, files in os.walk(obj_dir))
|
||||
print(f"[FOUND] {type_plural}/{obj_name}/ ({file_count} files)")
|
||||
|
||||
# --- 2. Reference check ---
|
||||
print()
|
||||
print("--- Reference check ---")
|
||||
|
||||
search_patterns = []
|
||||
|
||||
# 1) XML type references
|
||||
if obj_type in TYPE_REF_NAMES:
|
||||
for ref_name in TYPE_REF_NAMES[obj_type]:
|
||||
search_patterns.append(f"{ref_name}.{obj_name}")
|
||||
|
||||
# 2) BSL code references
|
||||
ru_mgr = TYPE_RU_MANAGER.get(obj_type)
|
||||
if ru_mgr:
|
||||
search_patterns.append(f"{ru_mgr}.{obj_name}")
|
||||
search_patterns.append(f"{type_plural}.{obj_name}")
|
||||
|
||||
# 3) CommonModule: method calls
|
||||
if obj_type == "CommonModule":
|
||||
search_patterns.append(f"{obj_name}.")
|
||||
|
||||
# 4) ScheduledJob/EventSubscription handler references
|
||||
if obj_type == "CommonModule":
|
||||
search_patterns.append(f"<Handler>{obj_name}.")
|
||||
search_patterns.append(f"<MethodName>{obj_name}.")
|
||||
|
||||
# Exclude object's own files
|
||||
exclude_dirs = []
|
||||
if has_dir:
|
||||
exclude_dirs.append(obj_dir)
|
||||
exclude_file = obj_xml if has_xml else ""
|
||||
|
||||
# Search all XML and BSL files
|
||||
references = []
|
||||
search_extensions = (".xml", ".bsl")
|
||||
|
||||
for root_path, dirs, files in os.walk(config_dir):
|
||||
for fname in files:
|
||||
ext = os.path.splitext(fname)[1].lower()
|
||||
if ext not in search_extensions:
|
||||
continue
|
||||
full_path = os.path.join(root_path, fname)
|
||||
|
||||
# Skip own files
|
||||
if exclude_file and os.path.normcase(full_path) == os.path.normcase(exclude_file):
|
||||
continue
|
||||
skip = False
|
||||
for ed in exclude_dirs:
|
||||
if os.path.normcase(full_path).startswith(os.path.normcase(ed + os.sep)) or os.path.normcase(full_path) == os.path.normcase(ed):
|
||||
skip = True
|
||||
break
|
||||
if skip:
|
||||
continue
|
||||
|
||||
# Get relative path
|
||||
rel_path = os.path.relpath(full_path, config_dir)
|
||||
rel_path_fwd = rel_path.replace("\\", "/")
|
||||
|
||||
# Skip auto-cleaned files
|
||||
if rel_path_fwd == "Configuration.xml" or rel_path_fwd == "ConfigDumpInfo.xml" or rel_path_fwd.startswith("Subsystems"):
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(full_path, "r", encoding="utf-8-sig") as fh:
|
||||
content = fh.read()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for pat in search_patterns:
|
||||
if pat in content:
|
||||
references.append({"File": rel_path, "Pattern": pat})
|
||||
break
|
||||
|
||||
# Also check Type.Name references
|
||||
type_name_ref = f"{obj_type}.{obj_name}"
|
||||
already_found_files = {r["File"] for r in references}
|
||||
|
||||
for root_path, dirs, files in os.walk(config_dir):
|
||||
for fname in files:
|
||||
if not fname.lower().endswith(".xml"):
|
||||
continue
|
||||
full_path = os.path.join(root_path, fname)
|
||||
|
||||
if exclude_file and os.path.normcase(full_path) == os.path.normcase(exclude_file):
|
||||
continue
|
||||
skip = False
|
||||
for ed in exclude_dirs:
|
||||
if os.path.normcase(full_path).startswith(os.path.normcase(ed + os.sep)) or os.path.normcase(full_path) == os.path.normcase(ed):
|
||||
skip = True
|
||||
break
|
||||
if skip:
|
||||
continue
|
||||
|
||||
rel_path = os.path.relpath(full_path, config_dir)
|
||||
rel_path_fwd = rel_path.replace("\\", "/")
|
||||
|
||||
if rel_path_fwd == "Configuration.xml" or rel_path_fwd == "ConfigDumpInfo.xml" or rel_path_fwd.startswith("Subsystems"):
|
||||
continue
|
||||
|
||||
if rel_path in already_found_files:
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(full_path, "r", encoding="utf-8-sig") as fh:
|
||||
content = fh.read()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if type_name_ref in content:
|
||||
references.append({"File": rel_path, "Pattern": type_name_ref})
|
||||
|
||||
if references:
|
||||
print(f"[WARN] Found {len(references)} reference(s) to {obj_type}.{obj_name}:")
|
||||
print()
|
||||
shown = 0
|
||||
for ref in references:
|
||||
print(f" {ref['File']}")
|
||||
print(f" pattern: {ref['Pattern']}")
|
||||
shown += 1
|
||||
if shown >= 20:
|
||||
remaining = len(references) - shown
|
||||
if remaining > 0:
|
||||
print(f" ... and {remaining} more")
|
||||
break
|
||||
print()
|
||||
|
||||
if not args.Force:
|
||||
print(f"[ERROR] Cannot remove: object has {len(references)} reference(s).")
|
||||
print(" Use -Force to remove anyway, or fix references first.")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("[WARN] -Force specified, proceeding despite references")
|
||||
else:
|
||||
print("[OK] No references found")
|
||||
|
||||
# --- 3. Remove from Configuration.xml ChildObjects ---
|
||||
print()
|
||||
print("--- Configuration.xml ---")
|
||||
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(config_xml, xml_parser)
|
||||
xml_root = tree.getroot()
|
||||
|
||||
cfg_node = xml_root.find(f"{{{MD_NS}}}Configuration")
|
||||
if cfg_node is None:
|
||||
print("[ERROR] Configuration element not found in Configuration.xml")
|
||||
errors += 1
|
||||
else:
|
||||
child_objects = cfg_node.find(f"{{{MD_NS}}}ChildObjects")
|
||||
if child_objects is not None:
|
||||
found = False
|
||||
for child in list(child_objects):
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
if localname(child) == obj_type and (child.text or "").strip() == obj_name:
|
||||
found = True
|
||||
if not args.DryRun:
|
||||
# Remove preceding whitespace (tail of previous sibling or text of parent)
|
||||
prev = child.getprevious()
|
||||
if prev is not None:
|
||||
if prev.tail and prev.tail.strip() == "":
|
||||
prev.tail = prev.tail.rsplit("\n", 1)[0] + "\n" if "\n" in prev.tail else ""
|
||||
if not prev.tail.strip():
|
||||
# Keep just the last newline+indent before the next element
|
||||
pass
|
||||
child_objects.remove(child)
|
||||
print(f"[OK] Removed <{obj_type}>{obj_name}</{obj_type}> from ChildObjects")
|
||||
actions += 1
|
||||
break
|
||||
if not found:
|
||||
print(f"[WARN] <{obj_type}>{obj_name}</{obj_type}> not found in ChildObjects")
|
||||
|
||||
# Save Configuration.xml
|
||||
if actions > 0 and not args.DryRun:
|
||||
save_xml_bom(tree, config_xml)
|
||||
print("[OK] Configuration.xml saved")
|
||||
|
||||
# --- 4. Remove from subsystem Content ---
|
||||
print()
|
||||
print("--- Subsystems ---")
|
||||
|
||||
subsystems_dir = os.path.join(config_dir, "Subsystems")
|
||||
subsystems_found = 0
|
||||
subsystems_cleaned = 0
|
||||
|
||||
def remove_from_subsystems(dir_path):
|
||||
nonlocal subsystems_found, subsystems_cleaned
|
||||
|
||||
if not os.path.isdir(dir_path):
|
||||
return
|
||||
|
||||
for fname in os.listdir(dir_path):
|
||||
if not fname.lower().endswith(".xml"):
|
||||
continue
|
||||
xml_file = os.path.join(dir_path, fname)
|
||||
if not os.path.isfile(xml_file):
|
||||
continue
|
||||
|
||||
ss_parser = etree.XMLParser(remove_blank_text=False)
|
||||
try:
|
||||
ss_tree = etree.parse(xml_file, ss_parser)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
ss_root = ss_tree.getroot()
|
||||
ss_node = None
|
||||
for child in ss_root:
|
||||
if isinstance(child.tag, str) and localname(child) == "Subsystem":
|
||||
ss_node = child
|
||||
break
|
||||
if ss_node is None:
|
||||
continue
|
||||
|
||||
props_node = ss_node.find(f"{{{MD_NS}}}Properties")
|
||||
if props_node is None:
|
||||
continue
|
||||
|
||||
content_node = props_node.find(f"{{{MD_NS}}}Content")
|
||||
if content_node is None:
|
||||
continue
|
||||
|
||||
ss_name_node = props_node.find(f"{{{MD_NS}}}Name")
|
||||
ss_name = ss_name_node.text if ss_name_node is not None and ss_name_node.text else os.path.splitext(fname)[0]
|
||||
|
||||
target_ref = f"{obj_type}.{obj_name}"
|
||||
modified = False
|
||||
|
||||
for item in list(content_node):
|
||||
if not isinstance(item.tag, str):
|
||||
continue
|
||||
val = (item.text or "").strip()
|
||||
if val == target_ref:
|
||||
subsystems_found += 1
|
||||
if not args.DryRun:
|
||||
content_node.remove(item)
|
||||
modified = True
|
||||
print(f"[OK] Removed from subsystem '{ss_name}'")
|
||||
subsystems_cleaned += 1
|
||||
|
||||
if modified and not args.DryRun:
|
||||
save_xml_bom(ss_tree, xml_file)
|
||||
|
||||
# Recurse into child subsystems
|
||||
base_name = os.path.splitext(fname)[0]
|
||||
child_dir = os.path.join(dir_path, base_name, "Subsystems")
|
||||
if os.path.isdir(child_dir):
|
||||
remove_from_subsystems(child_dir)
|
||||
|
||||
if os.path.isdir(subsystems_dir):
|
||||
remove_from_subsystems(subsystems_dir)
|
||||
if subsystems_cleaned == 0:
|
||||
print("[OK] Not referenced in any subsystem")
|
||||
else:
|
||||
print("[OK] No Subsystems directory")
|
||||
|
||||
# --- 5. Delete object files ---
|
||||
print()
|
||||
print("--- Files ---")
|
||||
|
||||
if not args.KeepFiles:
|
||||
if has_dir and not args.DryRun:
|
||||
shutil.rmtree(obj_dir)
|
||||
print(f"[OK] Deleted directory: {type_plural}/{obj_name}/")
|
||||
actions += 1
|
||||
elif has_dir:
|
||||
print(f"[DRY] Would delete directory: {type_plural}/{obj_name}/")
|
||||
actions += 1
|
||||
|
||||
if has_xml and not args.DryRun:
|
||||
os.remove(obj_xml)
|
||||
print(f"[OK] Deleted file: {type_plural}/{obj_name}.xml")
|
||||
actions += 1
|
||||
elif has_xml:
|
||||
print(f"[DRY] Would delete file: {type_plural}/{obj_name}.xml")
|
||||
actions += 1
|
||||
|
||||
if not has_xml and not has_dir:
|
||||
print("[OK] No files to delete")
|
||||
else:
|
||||
print("[SKIP] File deletion skipped (-KeepFiles)")
|
||||
|
||||
# --- Summary ---
|
||||
print()
|
||||
total_actions = actions + subsystems_cleaned
|
||||
if args.DryRun:
|
||||
print(f"=== Dry run complete: {total_actions} actions would be performed ===")
|
||||
else:
|
||||
print(f"=== Done: {total_actions} actions performed ({subsystems_cleaned} subsystem references removed) ===")
|
||||
|
||||
if errors > 0:
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user