[PowerShell] 纯文本查看 复制代码
#Requires -Version 5.1
<#
.SYNOPSIS
ArchiveToTomblist.ps1 v44-ps - WizTree Tomblist Generator
.DESCRIPTION
适配 DOpus {allfilepath};修复 StrictMode 下 $null.Count 崩溃;
UTF-8 管道防乱码;最长公共前缀推导;进程句柄安全释放。
.NOTES
Version: 43-ps
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromRemainingArguments = $true)]
[string[]]$ArchivePaths,
[string]$SevenZipPath = "C:\Program Files\7-Zip\7z.exe"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ============================================================================
# BLOCK 0: DOpus {allfilepath} 兼容解析 (v43-fix2)
# 说明: 四级降级策略,新增 Level 0 正则提取 Windows 绝对路径
# Level 0: 从混合字符串中正则提取所有 X:\...\ext 路径 (无视分隔符格式)
# Level 1: 标准双引号拼接 "C:\a.zip" "C:\b.zip"
# Level 2: 换行符分隔
# Level 3: 原始数组已正确拆分
# ============================================================================
$SafePaths = [System.Collections.ArrayList]::new()
if ($null -ne $ArchivePaths) {
$joinedRaw = $ArchivePaths -join "`n"
# Level 0: 正则提取所有 Windows 绝对路径 (最可靠,不依赖分隔符)
# 匹配: 盘符:\任意非空字符直到遇到常见压缩包扩展名
$pathPattern = '[A-Za-z]:\\[^\r\n"]+?\.(?:zip|7z|rar|tar|gz|bz2|xz|iso|cab|lzma|wim|esd)'
$regexMatches = [regex]::Matches($joinedRaw, $pathPattern, 'IgnoreCase')
if ($regexMatches.Count -gt 1) {
# 提取到多个路径 → 确认是拼接字符串,逐条加入
foreach ($m in $regexMatches) {
[void]$SafePaths.Add($m.Value.Trim())
}
} elseif ($joinedRaw -match '"\s+"') {
# Level 1: 标准双引号拼接
$split = $joinedRaw -split '"\s+"' | Where-Object { $_ -ne '' } | ForEach-Object { $_.Trim('"').Trim() }
foreach ($s in $split) { [void]$SafePaths.Add($s) }
} elseif ($joinedRaw -match '[\r\n]+') {
# Level 2: 换行符分隔
$split = $joinedRaw -split '[\r\n]+' | Where-Object { $_ -ne '' } | ForEach-Object { $_.Trim().Trim('"') }
foreach ($s in $split) { [void]$SafePaths.Add($s) }
} else {
# Level 3: 单路径或已正确拆分的数组
foreach ($p in $ArchivePaths) {
$cleaned = $p.Trim().Trim('"')
if (-not [string]::IsNullOrWhiteSpace($cleaned)) {
[void]$SafePaths.Add($cleaned)
}
}
}
}
$ArchivePaths = [string[]]@($SafePaths.ToArray())
# 调试输出 (验证通过后删除)
Write-Host "[DEBUG] Parsed $($ArchivePaths.Count) path(s):" -ForegroundColor DarkGray
foreach ($dp in $ArchivePaths) { Write-Host " -> $dp" -ForegroundColor DarkGray }
# ============================================================================
# BLOCK 1: 环境预检与全局变量初始化
# 说明: 校验 7z 路径,初始化时间戳与 UTF-8 NoBOM 编码器
# ============================================================================
if (-not (Test-Path -LiteralPath $SevenZipPath)) {
Write-Error "[ERROR] 7z.exe not found at: $SevenZipPath"
return
}
$NowStr = Get-Date -Format "yyyy/MM/dd HH:mm:ss"
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
# ============================================================================
# BLOCK 2: 主循环 - 逐包处理入口
# 说明: 遍历每个压缩包,解析路径并生成输出文件名
# ============================================================================
foreach ($Archive in $ArchivePaths) {
try {
$FullArchivePath = [System.IO.Path]::GetFullPath($Archive)
} catch {
Write-Warning "[SKIP] Invalid path: $Archive"
continue
}
if (-not (Test-Path -LiteralPath $FullArchivePath)) {
Write-Warning "[SKIP] File missing: $FullArchivePath"
continue
}
$ArchiveDir = [System.IO.Path]::GetDirectoryName($FullArchivePath)
$ArchiveBaseName = [System.IO.Path]::GetFileNameWithoutExtension($FullArchivePath)
$ArchiveExt = [System.IO.Path]::GetExtension($FullArchivePath)
$OutputFile = Join-Path $ArchiveDir "$ArchiveBaseName${ArchiveExt}.tomblist.csv"
$VRootName = "$ArchiveBaseName\"
Write-Host "`n===== Processing =====" -ForegroundColor Cyan
Write-Host "Input : $FullArchivePath"
Write-Host "Output: $OutputFile"
# ========================================================================
# BLOCK 3: 调用 7z 并流式解析记录
# 说明: UTF-8 管道编码;状态机逐行解析;try/finally 保证句柄释放
# ========================================================================
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $SevenZipPath
$psi.Arguments = "l -slt -sccUTF-8 -scsUTF-8 -y `"$FullArchivePath`""
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
$process = $null
$Records = [System.Collections.Generic.List[PSCustomObject]]::new()
try {
$process = [System.Diagnostics.Process]::Start($psi)
$CurrentRecord = @{}
$reader = $process.StandardOutput
while ($null -ne ($line = $reader.ReadLine())) {
# 空行 = 记录分隔符
if ([string]::IsNullOrWhiteSpace($line)) {
if ($CurrentRecord.Count -gt 0 -and $CurrentRecord.ContainsKey('Path')) {
$sizeVal = [uint64]0
$packedVal = [uint64]0
if ($CurrentRecord.ContainsKey('Size')) { [uint64]::TryParse($CurrentRecord['Size'], [ref]$sizeVal) | Out-Null }
if ($CurrentRecord.ContainsKey('Packed Size')) { [uint64]::TryParse($CurrentRecord['Packed Size'], [ref]$packedVal) | Out-Null }
$isFolder = ($CurrentRecord.ContainsKey('Folder') -and $CurrentRecord['Folder'] -eq '+') -or
($CurrentRecord.ContainsKey('Attributes') -and $CurrentRecord['Attributes'] -match '^D')
$modStr = $NowStr
if ($CurrentRecord.ContainsKey('Modified') -and -not [string]::IsNullOrWhiteSpace($CurrentRecord['Modified'])) {
$modStr = $CurrentRecord['Modified'].Replace('-', '/')
if ($modStr.Length -gt 19) { $modStr = $modStr.Substring(0, 19) }
}
$Records.Add([PSCustomObject]@{
RawPath = $CurrentRecord['Path']
Size = $sizeVal
PackedSize = $packedVal
Modified = $modStr
Attributes = if ($isFolder) { 16 } else { 32 }
IsFolder = $isFolder
})
}
$CurrentRecord = @{}
continue
}
# Key=Value 解析
$eqIndex = $line.IndexOf('=')
if ($eqIndex -gt 0) {
$key = $line.Substring(0, $eqIndex).Trim()
$val = $line.Substring($eqIndex + 1).TrimStart()
$CurrentRecord[$key] = $val
}
}
$process.WaitForExit()
if ($process.ExitCode -ne 0) {
$err = $process.StandardError.ReadToEnd()
Write-Warning "[WARN] 7z exited with code $($process.ExitCode): $err"
}
} finally {
if ($null -ne $process) { $process.Dispose() }
}
if ($Records.Count -eq 0) {
Write-Warning "[SKIP] No valid records parsed from archive."
continue
}
# ========================================================================
# BLOCK 4: 最长公共前缀推导
# 说明: @() 强制包裹过滤结果,防止 StrictMode 下 $null.Count 崩溃
# 逐级比对所有路径,提取最长公共父目录作为 VRoot 基准
# ========================================================================
$CommonPrefix = ""
$allPaths = @($Records | Where-Object { -not [string]::IsNullOrWhiteSpace($_.RawPath) } | Select-Object -ExpandProperty RawPath)
if ($allPaths.Count -gt 0) {
$parts = $allPaths[0] -split '\\'
for ($i = 0; $i -lt $parts.Count; $i++) {
$candidate = ($parts[0..$i] -join '\') + '\'
$allMatch = $true
foreach ($p in $allPaths) {
if (-not $p.StartsWith($candidate, [System.StringComparison]::OrdinalIgnoreCase)) {
$allMatch = $false
break
}
}
if ($allMatch) { $CommonPrefix = $candidate } else { break }
}
}
Write-Host "Detected Prefix: '$CommonPrefix'"
# ============================================================================
# BLOCK 5: 构建 CSV 内容 (v43-fix4 + __TOMBSTONE__ 路径嵌入)
# 说明: 将 __TOMBSTONE__ 作为虚拟子目录嵌入 VRootName 路径结构
# 输出格式: "VRootName\__TOMBSTONE__\原始绝对路径"
# ============================================================================
$sb = [System.Text.StringBuilder]::new()
[void]$sb.AppendLine("Generated by WizTree Tomblist Tool v43-ps")
[void]$sb.AppendLine('File Name,Size,Allocated,Modified,Attributes,Files,Folders')
[void]$sb.AppendLine("`"$VRootName`",0,0,$NowStr,0,0,0")
$fileCount = 0
$dirCount = 0
$seenDirs = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
$archiveSelfName = "$ArchiveBaseName$ArchiveExt"
foreach ($rec in $Records) {
$relPath = $rec.RawPath
if ($CommonPrefix -and $relPath.StartsWith($CommonPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
$relPath = $relPath.Substring($CommonPrefix.Length)
}
if ([string]::IsNullOrWhiteSpace($relPath)) { continue }
if ($rec.IsFolder) {
$dirPath = $relPath.TrimEnd('\', '.')
if ([string]::IsNullOrWhiteSpace($dirPath)) { continue }
if ($seenDirs.Add($dirPath)) {
[void]$sb.AppendLine("`"$VRootName$dirPath\`",0,0,$NowStr,16,0,0")
$dirCount++
}
} else {
$filePath = $relPath.TrimEnd('\', '.')
if ([string]::IsNullOrWhiteSpace($filePath)) { continue }
$alloc = if ($rec.Size -gt 0) { $rec.Size } else { $rec.PackedSize }
# ★ 判断是否为压缩包自身记录
$isArchiveSelf = ($filePath -eq $archiveSelfName) -or
($filePath.EndsWith("\$archiveSelfName", [System.StringComparison]::OrdinalIgnoreCase)) -or
($rec.RawPath -eq "$ArchiveBaseName$ArchiveExt") -or
([System.IO.Path]::GetFileName($rec.RawPath) -eq $archiveSelfName -and
-not ($CommonPrefix -and $rec.RawPath.StartsWith($CommonPrefix, [System.StringComparison]::OrdinalIgnoreCase)))
# ★ 核心改动: __TOMBSTONE__ 作为虚拟子目录嵌入路径
# 注意: 必须用 ${VRootName} 而非 $VRootName,否则 PS 会将
# $VRootName__TOMBSTONE__ 整体解析为一个未定义的变量名
$displayName = if ($isArchiveSelf) {
"${VRootName}__TOMBSTONE__\$($rec.RawPath)"
} else {
"${VRootName}$filePath"
}
[void]$sb.AppendLine("`"$displayName`",$($rec.Size),$alloc,$($rec.Modified),$($rec.Attributes),0,0")
$fileCount++
}
}
# ========================================================================
# BLOCK 6: 写入 UTF-8 NoBOM 文件并输出统计
# 说明: 使用 .NET API 直接写入,绕过 PowerShell 默认 BOM 行为
# ========================================================================
[System.IO.File]::WriteAllText($OutputFile, $sb.ToString(), $utf8NoBom)
Write-Host "[OK] Generated: $OutputFile" -ForegroundColor Green
Write-Host " Files: $fileCount | Dirs: $dirCount"
}
Write-Host "`n[DONE] All tasks finished." -ForegroundColor Yellow