如果这些文件正在进行更改(即正在下载的文件),Powershell 无法正确读取文件大小

Powershell doesn't read files size correctly if these are in progress of change (i.e files being downloaded)

我正在编写一个 Powershell 脚本,目的是在完成一个或多个文件的下载后关闭 Windows 机器 (laptop/PC)(为了举例,我们假设这里就是一个大文件)。

简而言之,我们每 x 秒读取一次整个下载文件夹的大小,并比较前后的延迟大小。如果最近一段时间没有变化,说明下载完成或者卡住,两种情况都会导致关机。

给定以下脚本(大小 getter 在某些时候可以是独立函数,是的):

#Powershell5
#Major  Minor  Build  Revision
# -----  -----  -----  --------
# 5      1      19041  1320  

$downloadDir = "C:\Users\edi\Downloads"

while (1) 
{
    $s1 = Get-ChildItem -Path $downloadDir | Measure-Object -Property Length -Sum | Select-Object Sum
    write-host "S1:" $s1.sum

    # this 3 seconds time is just for testing purposes; in real case scenario this will most
    # likely be set to 60 or 120 seconds.
    Start-Sleep -s 3

    $s2 = Get-ChildItem -Path $downloadDir | Measure-Object -Property Length -Sum | Select-Object Sum
    write-host "S2:" $s2.sum

    if ($s1.sum -eq $s2.sum) 
    {
        write-host "Download complete, shutting down.."
        # loop exit is this, actual shutdown; commented out for testing purposes.
        #shutdown /s
    } 
}

我面临的问题是文件大小读取不是“实时”完成的。换句话说,文件大小不会像您通常在资源管理器视图中看到的那样发生变化。我需要能够实时读取这些数字(更改文件大小)。

有趣的事实: 当下载和脚本 运行 时,如果手动转到下载文件夹并按 F5 / 刷新...数字变化(尺寸读数准确)。

旁注:我的研究让我看到了这篇可能提出根本原因的文章,但我不是 100% 确定它:https://devblogs.microsoft.com/oldnewthing/20111226-00/?p=8813

感谢任何关于此的想法。提前致谢!

此答案旨在证明,即使不可靠,也可以实时监控文件大小。在这种情况下,[System.IO.StreamWriter] 正在写入一个文件,每次迭代都会添加 1 个字节,直到文件达到 1Mb。

我也进行了同样的测试while downloading a "test file",它对我来说工作正常,文件夹的总大小正在正确更新。

您观察到的情况的可能原因完全基于假设:

  • 正在下载的文件已预先分配 space - 我不知道这怎么可能,因为正如您所解释的,通过查看资源管理器,您可以看到文件大小在增加。
  • 之前的 大小与 计算之后的 大小之间没有足够的时间 - 这可能是由于缓冲, 已经详细解释了这一点。增加睡眠时间可能会解决这个问题。另外,调用了我不知道的 .Refresh() 方法,直到谢谢 :)

我的想法,我个人认为这不是解决这个问题的正确方法。进程监控将是一种更好的方法(假设这是一种可能性 - 也由 mklement0 解释,我完全同意他的看法)。

$checkSize = {
    (Get-ChildItem $PWD -File | Measure-Object -Property Length -Sum).Sum
}

$currentSize = & $checkSize
$testFile = Join-Path $pwd -ChildPath "testfile.dump"

$job = Start-Job {

    $writer = [System.IO.StreamWriter]::new(   
        [System.IO.File]::Create($using:testFile)
    )
    0..1Mb | ForEach-Object { $writer.Write(1) }
    $writer.Close()

} -Name testDump

'
Starting test:
'

do
{
    $increasingSizeBefore = & $checkSize
    Start-Sleep -Seconds 2
    $increasingSizeAfter = & $checkSize

    'StartingSize: {0} - SizeBefore: {1} - SizeAfter: {2}' -f
    $currentSize, $increasingSizeBefore, $increasingSizeAfter

} until ($increasingSizeBefore -eq $increasingSizeAfter)

$job | Stop-Job -PassThru | Remove-Job

我的结果:

Starting test:

StartingSize: 17458 - SizeBefore: 17458 - SizeAfter: 152626
StartingSize: 17458 - SizeBefore: 152626 - SizeAfter: 406578
StartingSize: 17458 - SizeBefore: 406578 - SizeAfter: 652338
StartingSize: 17458 - SizeBefore: 652338 - SizeAfter: 902194
StartingSize: 17458 - SizeBefore: 902194 - SizeAfter: 1066035
StartingSize: 17458 - SizeBefore: 1066035 - SizeAfter: 1066035

我建议采用不同的策略:

  • 为每个下载过程设置一个整体超时,例如curl.exe--max-time选项。

  • 不幸的是,PowerShell 自己的 Invoke-WebRequestInvoke-RestMethod 似乎只有 connection 超时(-TimeoutSec),不是整体连接超时。

这样您就可以跟踪下载 进程 ,并在所有进程终止(无论是由于完成还是超时)后触发重新启动。


至于你的做法:

  • 您可以通过 Get-ChildItem 查询的 磁盘文件大小 未更新 连续 当一个文件正在被写入时,正如您所观察到的,并且当它最终更新时,可能不会发生,直到该文件已 关闭 ,即 完整 .

  • 但是,您可以根据需要更新文件大小信息,即通过System.IO.FileSystemInfo.Refresh()方法,这相当于您通过文件资源管理器执行的手动刷新。

    • 但是请注意,由于内部缓冲 写入,这仍然不是实时 大小信息。
# Perform this before every Measure-Object call.
# It refreshes the size information of all files in the specified dir.
(Get-ChildItem -File -LiteralPath $downloadDir).Refresh()

顺便说一句:正如圣地亚哥指出的那样,这种方法从根本上不适用于预分配具有下载完整大小的输出文件的下载实用程序/API,这显然是某些 BitTorrent 客户端提供的功能。

至于缩小已完成的下载与假设卡住的下载的范围:

Invoke-WebRequest / Invoke-RestMethod 在下载过程中专门锁定其输出文件,因此您可以进行读取尝试以查看无法读取哪些文件,从中可以推断出哪些下载,如果有,仍在进行中:

# Note: In PowerShell (Core) 7+, use -AsByteStream instead of -Encoding Byte
Get-ChildItem -File -LiteralPath $downloadDir | 
  Get-Content -Encoding Byte -First 1 -ErrorVariable errs -ErrorAction SilentlyContinue |
    Out-Null

if ($errs) { Write-Warning "Incomplete downloads:`n$($errs.TargetObject)" }