get-ftp.ps1
function Get-FTP { <# .Synopsis Gets files from FTP .Description Lists files on an FTP server, or downloads files .Example Get-FTP -FTP "ftp://edgar.sec.gov/edgar/full-index/1999/" -Download -Filter "*.idx", "*.xml" .Example Get-FTP -FTP "ftp://edgar.sec.gov/edgar/full-index/1999/" -Download -Filter "*.idx", "*.xml" -DownloadAsJob .Link Push-FTP #> [OutputType([IO.FileInfo])] [CmdletBinding(DefaultParameterSetName='FTPSite')] param( # The root url of an FTP server [Parameter(Mandatory=$true,Position=0,ValueFromPipelineByPropertyName=$true,ParameterSetName='FTPSite')] [Alias('FTP')] [Uri]$FtpRoot, # A list of specific files on an FTP server. Useful for when dealing with FTP servers that do not allow listing. [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName='FTPFile')] [Uri[]] $FtpFile, # The credential used to connect to FTP. If not provided, will connect anonymously. [Parameter(ValueFromPipelineByPropertyName=$true)] [Management.Automation.PSCredential] $Credential, # If set, will download files instead of discover them [Parameter(ValueFromPipelineByPropertyName=$true,ParameterSetName='FTPSite')] [Switch]$Download, # The download path (by default, the downloads directory) [Parameter(ValueFromPipelineByPropertyName=$true)] [string]$DownloadPath = "$env:UserProfile\Downloads", # If provided, will only download files that match the filter [Parameter(ValueFromPipelineByPropertyName=$true,Position=1,ParameterSetName='FTPSite')] [string[]]$Filter, # If set, will download files that already have been downloaded and have the exact same file size. [Parameter(ValueFromPipelineByPropertyName=$true,ParameterSetName='FTPSite')] [Switch]$Force, # If set, downloads will run as a background job [Parameter(ValueFromPipelineByPropertyName=$true)] [Switch] $DownloadAsJob, # If set, downloads will be run in parallel in a PowerShell workflow [Parameter(ValueFromPipelineByPropertyName=$true)] [Switch] $UseWorkflow, # If set, download progress will not be displayed [Parameter(ValueFromPipelineByPropertyName=$true)] [Switch] $HideProgress, # The size of the copy buffer. By default, this is 50kb [Uint32] $BufferSize = 50kb ) begin { $folders = New-Object "system.collections.generic.queue[string]" $files= New-Object "system.collections.generic.queue[string]" $GetFtpStream = { param($url, $method, $Credential) try { $ftpRequest = [System.Net.FtpWebRequest]::Create($url) if ($Credential) { $ftpRequest.Credentials = $Credential.GetNetworkCredential() } $ftpRequest.Method = $method $ftpresponse = $ftpRequest.GetResponse() $reader = New-Object IO.StreamReader $ftpresponse.GetResponseStream() while (($line = $reader.ReadLine()) -ne $null) { $line.Trim() } if ($reader) { $reader.Dispose() } if ($ftpresponse.Close) { $ftpresponse.Close() } } catch { $err = $_ Write-Error -Exception $err.Exception -Message "Error in FTP $method Request on $url" return } finally { } } $GetFtpFile = { param($Source,$Target,$Credential, $HideProgress, $BufferSize) try { $FileSize = try { $ftprequest = [Net.FtpWebRequest]::create($Source) if ($Credential) { $ftprequest.Credentials = $Credential.GetNetworkCredential() } $ftprequest.Method = [Net.WebRequestMethods+Ftp]::GetFileSize $ftpresponse = $ftprequest.GetResponse() $ftpresponse.ContentLength if ($ftpresponse.Close) { $ftpresponse.Close() } } catch { } $ftprequest = [Net.FtpWebRequest]::create($Source) if ($Credential) { $ftprequest.Credentials = $Credential.GetNetworkCredential() } $ftprequest.Method = [Net.WebRequestMethods+Ftp]::DownloadFile $ftprequest.UseBinary = $true $ftprequest.KeepAlive = $false $ftpresponse = $ftprequest.GetResponse() $responsestream = $ftpresponse.GetResponseStream() if (-not $responsestream) { return } $targetfile = New-Object IO.FileStream ($Target,[IO.FileMode]::Create) [byte[]]$readbuffer = New-Object byte[] $BufferSize $perc = 0 $progressId = Get-Random do{ $readlength = $responsestream.Read($readbuffer,0,$BufferSize) $targetfile.Write($readbuffer,0,$readlength) if ($FileSize) { if (-not $HideProgress) { $perc = $targetfile.Position * 100 / $FileSize Write-Progress "Downloading $Source" "To $Target" -PercentComplete $perc -Id $progressId } } else { if (-not $HideProgress) { $perc += 5 if ($perc -gt 100) { $perc = 0 } Write-Progress "Downloading $Source" "To $Target" -PercentComplete $perc -Id $progressId } } } while ($readlength -ne 0) $targetfile.close() if ($ftpresponse.Close) { $ftpresponse.Close() } Write-Progress "Downloading $Source" "To $Target" -Completed -Id $progressId Get-Item -Path $target } catch { $err = $_ Write-Error -Exception $err.Exception -Message "FTP Error Downloading $source - $($err.Exception.Message)" return } } $jobDefintion = [ScriptBlock]::Create(@" param([Hashtable]`$Parameter) `$getFtpFile = { $GetFtpFile } & `$getFtpFile @parameter "@) $workflowDefinition = @" workflow getFtpFilesWorkflow( [Parameter(Position=0)] [Hashtable[]]`$ftpFileInput ) { foreach -parallel (`$ftpFile in `$ftpFileInput) { `$ftpFile | inlineScript { `$parameter = `$(`$input) & { $GetFtpFile } @parameter } } } "@ . ([ScriptBlock]::create($workflowDefinition)) $Ftpjobs = @() $AsyncDownloadPaths = @() } process { if ($PSCmdlet.ParameterSetName -eq 'FTPSite') { $null = $folders.Enqueue("$ftpRoot") } elseif ($PSCmdlet.ParameterSetName -eq 'FTPFile') { foreach ($f in $FtpFile) { $null = $files.Enqueue("$f") } } } end { $workFlowInputData = @() while($folders.Count -gt 0 -or $RunningFtpjobs.Count -gt 0 -or $files.Count -gt 0){ if ($PSCmdlet.ParameterSetName -eq 'FTPSite' -and $folders.Count) { $fld = $folders.Dequeue() $newFiles = New-Object "system.collections.generic.list[string]" $newDirs = New-Object "system.collections.generic.list[string]" $operation = [System.Net.WebRequestMethods+Ftp]::ListDirectory foreach ($line in . $GetFtpStream $fld $operation $Credential 2>&1) { if ($line -is [Management.Automation.ErrorRecord]) { $line | Write-Error } else { [void]$newFiles.Add($line.Trim()) } } $operation = [System.Net.WebRequestMethods+Ftp]::ListDirectoryDetails foreach ($line in . $GetFtpStream $fld $operation $Credential 2>&1) { if ($line -is [Management.Automation.ErrorRecord]) { $line | Write-Error } else { [void]$newDirs.Add($line.Trim()) } } foreach ($d in $newDirs) { $parts = @($d -split " " -ne '') if ($parts.Count -eq 2) { # First line, purely informational continue } if ($parts.Count -eq 9) { # 9 parts. It's likely that this is a linux based FTP # The last part should be the file name # The preceeding 3 parts should be the modification time # The preceeding 1 part should be the file size if ($parts[-1] -eq '.' -or $parts[-1] -eq '..') { continue } $FileName = $parts[-1] $FileSize = $parts[-5] $FileDate = ((@($parts[-4..-3]) + (Get-Date).Year + $parts[-2]) -join ' ') -as [datetime] } elseif ($parts.Count -eq 4) { # First two parts should be date # Third part should be file length # Last part should be file name $FileName= $parts[-1] $FileSize = $parts[-2] $FileDate = ($parts[0..1] -join ' ') -as [DateTime] } if (-not $FileName) {continue } if ($FileSize -eq 4096 -or -not $FileSize) { $newName = $parts[-1] Write-Verbose "Enqueing Folder $($fld + $FileName + "/")" $null = $folders.Enqueue($fld + $FileName + "/") } $out = New-Object PSObject -Property @{ Ftp = $fld + "/" + $FileName Size = $FileSize UpdatedAt = $FileDate } if ($filter) { $matched = $false foreach ($f in $filter) { if ($FileName -like "$f") { $matched = $true break } } if (-not $matched) { continue } } if ($download -or $psBoundParameters.DownloadPath) { $folderUri = [uri]("$fld".TrimEnd("/") + "/" + $FileName) $downloadTo = Join-Path $DownloadPath $folderUri.LocalPath $downloadDir = Split-Path $downloadTo if (-not (Test-Path $downloadDir)) { $null = New-Item -ItemType Directory $downloadDir } $item = Get-Item -Path $downloadTo -ErrorAction SilentlyContinue if (($item.Length -ne $FileSize) -or $Force) { if ($DownloadAsJob) { $ftpJobs += Start-Job -ArgumentList @{ Source =$folderUri Target = $downloadTo Credential = $Credential HideProgress = $HideProgress BufferSize = $BufferSize } -ScriptBlock $jobDefintion } elseif ($UseWorkflow) { $workFlowInputData += @{ Source =$folderUri Target = $downloadTo Credential = $Credential HideProgress = $HideProgress BufferSize = $BufferSize } } else { & $GetFtpFile -Source $folderUri -Target $downloadTo -Credential:$Credential -BufferSize $BufferSize -HideProgress:$HideProgress } } } else { $out } } } elseif ($PSCmdlet.ParameterSetName -eq 'FTPFile' -and $files.Count) { $file= $files.Dequeue() $folderUri =[URI]$file $downloadTo = Join-Path $DownloadPath $folderUri.LocalPath $downloadDir = Split-Path $downloadTo if (-not (Test-Path $downloadDir)) { $null = New-Item -ItemType Directory $downloadDir } if ($DownloadAsJob) { $ftpJobs += Start-Job -ArgumentList @{ Source =$folderUri Target = $downloadTo Credential = $Credential HideProgress = $HideProgress BufferSize = $BufferSize } -ScriptBlock $jobDefintion } elseif ($UseWorkflow) { $workFlowInputData += @{ Source =$folderUri Target = $downloadTo Credential = $Credential HideProgress = $HideProgress BufferSize = $BufferSize } } else { $FileResults = & $GetFtpFile -Source $folderUri -Target $downloadTo -Credential:$Credential -BufferSize $BufferSize -HideProgress:$HideProgress 2>&1 if ($FileResults -is [Management.Automation.ErrorRecord]) { $FileResults | Write-Error } else { $FileResults } } } if ($Ftpjobs) { $Ftpjobs | Receive-Job $RunningFtpjobs = @($Ftpjobs | Where-Object { $_.JobStateInfo.State -ne 'Completed' }) } } while ($workFlowInputData.Count -or $RunningFtpjobs) { if ($workFlowInputData) { $Ftpjobs += getFtpFilesWorkflow $workFlowInputData -asjob } $workFlowInputData = $null if ($Ftpjobs) { $Ftpjobs | Receive-Job $RunningFtpjobs = @($Ftpjobs | Where-Object { $_.JobStateInfo.State -ne 'Completed' }) } } if ($Ftpjobs) { $Ftpjobs | Receive-Job $Ftpjobs | Remove-Job } } } |