PowerShell 分割和合並大文件


分割文件進度條

PowerShell 分割和合並大文件 3


背景

有同事提出將一個8G多的VHD上傳到百度的網盤,但是百度網盤針對免費用戶暫時的單個文件限制爲4G,已經挺大的了,但是還是裝不下這個VHD。最經濟的最方便的方法就是將大文件分割成小文件,既解決了無法上傳的問題,也降低了中途上傳失敗的概率(儘管支持斷點續傳)。假如網絡帶寬允許,將操作系統安裝在網盤上,也是一件挺有意思的事情。下次使用時,再下載多個文件,然後合併。

需求

  • 寫一個PowerShell 腳本,至少包含兩個函數“分割文件”和“合併文件”;
  • 分割文件可以按照文件個數分割,也可以按照單個文件的大小分割;
  • 將腳本處理時佔用的內存最大控制在50M左右;
  • 合併文件時接受待合併文件所在的目錄爲參數,並支持通配符過濾;
  • 在處理文件時,能夠顯示進度提示。

技術要點

  • 處理的一般都是大文件,所以使用.NET 中 FileStream 對象,因爲流處理可以提高性能。
  • 將緩衝區設置爲1M-50M,當分割的單個文件大小超過1G時,使用50M內存,小於等於1M時,使用1M,其餘按比例增加力求節省內存。
  • 暫時不考慮使用並行處理,因爲在此場景中性能更多由硬盤的讀取速度決定。
  • 分割出的文件在源文件名稱後追加_part_001 這樣的格式,方便在合併前按照升序排序。

截圖

進度條顯示

分割文件進度條

分割文件進度條

分割後的文件列表

分割後的文件列表

分割後的文件列表

使用示例

# 先導入 函數 Split-Merge-File
'E\Split-Merge-File.ps1' #注意句號和腳本之間有空格
 
# 將文件 ‘E:\win2012.vhdx’ 分割成20個小文件,輸出至目錄 'E:\VHD'
Split-File -File 'E:\win2012.vhdx' -ByPartCount -PartCount 20 -OutputDir 'E:\VHD'
 
# 將件‘E:\win2012.vhdx’按照每個大小 500MB 來分割,輸出至目錄 'E:\VHD'
Split-File -File 'E:\win2012.vhdx' -ByPartLength -PartLength 500MB -OutputDir 'E:\VHD'
 
# 將 'E:\VHD' 目錄下包含 part 的所有文件合併,輸出爲 單個文件 'E:\win2012-2.vhdx'
Merge- File -SourceDir 'E:\VHD' -Filter "*part*" -OutputFile 'E:\win2012-2.vhdx'

源腳本(Split-Merge-File.ps1)

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
# Obtain a suitable buffer length by partial file length
function Get-BufferLength ([int]$partialFileLength)
{
    [int]$MinBufferLength = 1MB
    # No need to consume great amount memory,initialize as 50M, you can adjust it from here.
    [int]$MaxBufferLength = 50MB
 
    if($partialFileLength -ge 1GB) { return $MaxBufferLength}
    elseif$partialFileLength -le 50MB) { return $MinBufferLength }
    elsereturn [int]$MaxBufferLength/1GB * $partialFileLength )}
}
 
# Write partial stream to file from current position
function Write-PartialStreamToFile
{
    param(
    [IO.FileStream]$stream,
    [long]$length,
    [string]$outputFile
    )
 
    #copy stream to file
    function Copy-Stream[int]$bufferLength )
    {
        [byte[]]$buffer New-Object byte[]( $bufferLength )
 
        # Read partial file data to memory buffer
        $stream.Read($buffer,0,$buffer.Length) | Out-Null
 
        # Flush buffer to file
        $outStream New-Object IO.FileStream($outputFile,'Append','Write','Read')
        $outStream.Write($buffer,0,$buffer.Length)
        $outStream.Flush()
        $outStream.Close()
    }
 
    $maxBuffer Get-BufferLength $length
    $remBuffer = 0
    $loop [Math]::DivRem($length,$maxBuffer,[ref]$remBuffer)
 
    if($loop -eq 0)
    {
        Copy-Stream $remBuffer
        return
    }
 
    1..$loop foreach {
        $bufferLength $maxBuffer
 
        # let last loop contains remanent length
        if( ($_ -eq $loop-and ($remBuffer -gt 0) ) 
        {
            $bufferLength $maxBuffer $remBuffer
         }
         Copy-Stream  $bufferLength
 
         # show outer progress
         $progress [int]($_*100/$loop)
         write-progress -activity 'Writting file' -status 'Progress' -id 2 -percentcomplete $progress -currentOperation "$progress %"
    }
}
 
# split a large file into mutiple parts by part count or part length
function Split-File
{
    param(
    [Parameter(Mandatory=$True)]
    [IO.FileInfo]$File,
    [Switch]$ByPartCount,
    [Switch]$ByPartLength,
    [int]$PartCount,
    [int]$PartLength,
    [IO.DirectoryInfo]$OutputDir '.'
    )
 
    # Argument validation
    if(-not $File.Exists) { throw "Source file [$File] not exists" }
    if(-not  $OutputDir.Exists) { mkdir $OutputDir.FullName | Out-Null}
    if( (-not $ByPartCount-and (-not $ByPartLength) )
    {
        throw 'Must specify one of parameter, [ByPartCount] or [ByPartLength]'
    }
    elseif$ByPartCount )
    {
        if($PartCount -le 1) {throw '[PartCount] must larger than 1'}
        $PartLength $File.Length / $PartCount
    }
    elseif$ByPartLength )
    {
        if($PartLength -lt 1) { throw '[PartLength] must larger than 0' }
        if($PartLength -ge $File.Length) { throw '[PartLength] must less than source file' }
        $temp $File.Length /$PartLength
        $PartCount [int]$temp
        if( ($File.Length % $PartLength-gt -and $PartCount -lt $temp ) )
        {
          $PartCount++
        }
    }
 
    $stream New-Object IO.FileStream($File.FullName,
    [IO.FileMode]::Open ,[IO.FileAccess]::Read ,[IO.FileShare]::Read )
 
    # Make sure each part file name ended like '001' so that it's convenient to merge
    [string]$numberMaskStr [string]::Empty.PadLeft( [int]([Math]::Log10($PartCount) + 1), "0" )
 
    1 .. $PartCount foreach {
         $outputFile Join-Path $OutputDir "{0}.part_{1} " -f $File.Name ,$_.ToString( $numberMaskStr ) )
         # show outer progress
         $progress [int]($_*100/$PartCount)
         write-progress -activity "Splitting file" -status "Progress $progress %" -Id 1 -percentcomplete $progress -currentOperation "Handle file $outputFile"
         if($_ -eq $PartCount)
         {
            Write-PartialStreamToFile $stream ($stream.Length - $stream.Position)$outputFile
         }
         else
         {
         Write-PartialStreamToFile $stream $PartLength  $outputFile
         }
    }
    $stream.Close()
}
 
function Merge-File
{
    param(
    [Parameter(Mandatory=$True)]
    [IO.DirectoryInfo]$SourceDir,
    [string]$Filter,
    [IO.FileInfo]$OutputFile
    )
 
    # arguments validation
    if -not $SourceDir.Exists ) { throw "Directory $SourceDir not exists." }
    $files = dir $SourceDir -File -Filter $Filter
    if($files -eq $null){ throw "No matched file in directory $SourceDir"}
 
    # output stream
    $outputStream New-Object IO.FileStream($OutputFile.FullName,
        [IO.FileMode]::Append ,[IO.FileAccess]::Write ,[IO.FileShare]::Read )
 
    # merge file
    $files foreach{
        #input stream
        $inputStream New-Object IO.FileStream($_.FullName,
        [IO.FileMode]::Open ,[IO.FileAccess]::Read ,[IO.FileShare]::Read )
 
        $bufferLength Get-BufferLength -partialFileLength $_.Length
        while($inputStream.Position -lt $inputStream.Length)
        {
            if( ($inputStream.Position $bufferLength-gt $inputStream.Length)
            {
                $bufferLength $inputStream.Length - $inputStream.Position
            }
 
            # show outer progress
            $progress [int]($inputStream.Position *100/ $inputStream.Length)
            write-progress -activity 'Merging file' -status "Progress $progress %"  -percentcomplete $progress
 
            # read file to memory buffer
            $bufferNew-Object byte[]( $bufferLength )
            $inputStream.Read( $buffer,0,$buffer.Length) | Out-Null
 
            #flush buffer to file
            $outputStream.Write( $buffer,0,$buffer.Length) | Out-Null
            $outputStream.Flush()
        }
        $inputStream.Close()
    }
    $outputStream.Close()
}
本文鏈接: http://www.pstips.net/powershell-split-merge-large-file.html
請尊重原作者和編輯的辛勤勞動,歡迎轉載,並註明出處!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章