.netpowershellftpftpwebrequest

PowerShell How can I delete a directory with FTP?


From the this list of Web Request Method Fields, I've successfully deleted files from my server via FTP, by basically filling in the guide on this page with minor alterations:

$url = "ftp://server.net/path/to/place/FILE.txt"
$userName = "username"
$password = "p@ssw0rd!"

$ftpreq = [System.Net.FtpWebRequest]::create($url)
$ftpreq.Credentials = New-Object System.Net.NetworkCredential($userName, $password)
$ftpreq.Method = [System.Net.WebRequestMethods+Ftp]::DeleteFile

$ftpreq.GetResponse()

The file is named FILE.txt, the folder is named FOLDER.

When I try to do something similar, but for a folder, I get PowerShell errors and 550 response.

I've tried basically two variations on the file deletion method.

Try 1: Change URL to match folder name, keep method

Note that the folder I'm attempting to delete is next to the file I have already successfully deleted.

$url = "ftp://server.net/path/to/place/FOLDER"
$userName = "username"
$password = "p@ssw0rd!"

$ftpreq = [System.Net.FtpWebRequest]::create($url)
$ftpreq.Credentials = New-Object System.Net.NetworkCredential($userName, $password)
$ftpreq.Method = [System.Net.WebRequestMethods+Ftp]::DeleteFile

$ftpreq.GetResponse()

Exception calling "GetResponse" with "0" argument(s): "The remote server returned an error: (550) File unavailable (e.g., file not found, no access).

Try 2: Change method

$url = "ftp://server.net/path/to/place/FOLDER"
$userName = "username"
$password = "p@ssw0rd!"

$ftpreq = [System.Net.FtpWebRequest]::create($url)
$ftpreq.Credentials = New-Object System.Net.NetworkCredential($userName, $password)
$ftpreq.Method = [System.Net.WebRequestMethods+Ftp]::RemoveDirectory

$ftpreq.GetResponse()

Exception calling "GetResponse" with "0" argument(s): "The remote server returned an error: (550) File unavailable (e.g., file not found, no access)."


Solution Implemented

function DeleteFtpFolder($url, $credentials)
{
    $listRequest = [Net.WebRequest]::Create($url)
    $listRequest.Method = [System.Net.WebRequestMethods+FTP]::ListDirectoryDetails
    $listRequest.Credentials = $credentials

    $lines = New-Object System.Collections.ArrayList

    $listResponse = $listRequest.GetResponse()
    $listStream = $listResponse.GetResponseStream()
    $listReader = New-Object System.IO.StreamReader($listStream)

    while (!$listReader.EndOfStream)
    {
        $line = $listReader.ReadLine()
        $lines.Add($line) | Out-Null
    }

    $listReader.Dispose()
    $listStream.Dispose()
    $listResponse.Dispose()

    foreach ($line in $lines)
    {
        $tokens = $line.Split(" ", 5, [System.StringSplitOptions]::RemoveEmptyEntries)

        $type = $tokens[2]
        $name = $tokens[3]
        $fileUrl = ($url + "/" + $name)

        if ($type -eq "<DIR>")
        {
            Write-Host "Found folder: $name"

            DeleteFtpFolder $fileUrl $credentials

            Write-Host "Deleting folder: $name"
            $deleteRequest = [Net.WebRequest]::Create($fileUrl)
            $deleteRequest.Credentials = $credentials
            $deleteRequest.Method = [System.Net.WebRequestMethods+FTP]::RemoveDirectory
            $deleteRequest.GetResponse() | Out-Null
        }
        else 
        {
            $fileUrl = ($url + "/" + $name)
            Write-Host "Deleting file: $name"

            $deleteRequest = [Net.WebRequest]::Create($fileUrl)
            $deleteRequest.Credentials = $credentials
            $deleteRequest.Method = [System.Net.WebRequestMethods+FTP]::DeleteFile
            $deleteRequest.GetResponse() | Out-Null
        }
    }
}

$credentials = New-Object System.Net.NetworkCredential($AzureFtpUsername, $AzureFtpPassword)
$url = $AzureFtpUrl

DeleteFtpFolder $url $credentials

The setup is an embedded PowerShell script inside a process step inside Octopus Deploy. That's why I call the function at the bottom.

This solution is nearly identical to the accepted answer, with some minor alterations on where I put the recursive call and how the returned data from the server gets parsed. His solution seems like it looks more like an ls output, with more columns, whereas mine looked somewhat more like a dir output, with fewer columns. But even then, it didn't look like the dir output on my local windows machine, so I'm not really sure exactly what is going on. But it works, so that's good enough.


Solution

  • RMD FTP command (RemoveDirectory method) fails, if the directory is not empty.

    Typically it fails with an error like:

    550 Directory not empty.

    Unfortunately FtpWebRequest has a bad habit of "translating" the FTP error codes to its own messages. In this case, it "translates" 550 to:

    File unavailable (e.g., file not found, no access).

    What hides the real problem.


    Anyway, there's no support for recursive operations in the FtpWebRequest class (or any other FTP implementation in the .NET framework). You have to implement the recursion yourself:

    Tricky part is to identify files from subdirectories. There's no way to do that in a portable way with the FtpWebRequest. The FtpWebRequest unfortunately does not support the MLSD command, which is the only portable way to retrieve directory listing with file attributes in FTP protocol. See also Checking if object on FTP server is file or directory.

    Your options are:

    function DeleteFtpFolder($url, $credentials)
    {
        $listRequest = [Net.WebRequest]::Create($url)
        $listRequest.Method = [System.Net.WebRequestMethods+Ftp]::ListDirectoryDetails
        $listRequest.Credentials = $credentials
    
        $lines = New-Object System.Collections.ArrayList
    
        $listResponse = $listRequest.GetResponse()
        $listStream = $listResponse.GetResponseStream()
        $listReader = New-Object System.IO.StreamReader($listStream)
        while (!$listReader.EndOfStream)
        {
            $line = $listReader.ReadLine()
            $lines.Add($line) | Out-Null
        }
        $listReader.Dispose()
        $listStream.Dispose()
        $listResponse.Dispose()
    
        foreach ($line in $lines)
        {
            $tokens = $line.Split(" ", 9, [StringSplitOptions]::RemoveEmptyEntries)
            $name = $tokens[8]
            $permissions = $tokens[0]
    
            $fileUrl = ($url + $name)
    
            if ($permissions[0] -eq 'd')
            {
                DeleteFtpFolder ($fileUrl + "/") $credentials
            }
            else
            {
                Write-Host "Deleting file $name"
                $deleteRequest = [Net.WebRequest]::Create($fileUrl)
                $deleteRequest.Credentials = $credentials
                $deleteRequest.Method = [System.Net.WebRequestMethods+Ftp]::DeleteFile
                $deleteRequest.GetResponse() | Out-Null
            }
        }
    
        Write-Host "Deleting folder"
        $deleteRequest = [Net.WebRequest]::Create($url)
        $deleteRequest.Credentials = $credentials
        $deleteRequest.Method = [System.Net.WebRequestMethods+Ftp]::RemoveDirectory
        $deleteRequest.GetResponse() | Out-Null
    }
    

    Use the function like:

    $url = "ftp://ftp.example.com/path/to/folder/";
    $credentials = New-Object System.Net.NetworkCredential("username", "password")
    DeleteFtpFolder $url $credentials
    

    Or use a 3rd party library that supports recursive operations.

    For example with WinSCP .NET assembly you can delete whole directory with a single call to Session.RemoveFiles:

    # Load WinSCP .NET assembly
    Add-Type -Path "WinSCPnet.dll"
    
    # Setup session options
    $sessionOptions = New-Object WinSCP.SessionOptions -Property @{
        Protocol = [WinSCP.Protocol]::Ftp
        HostName = "ftp.example.com"
        UserName = "username"
        Password = "password"
    }
    
    $session = New-Object WinSCP.Session
    
    # Connect
    $session.Open($sessionOptions)
    
    # Remove folder
    $session.RemoveFiles("/path/to/folder").Check()
    
    # Disconnect, clean up
    $session.Dispose()
    

    Internally, WinSCP uses the MLSD command, if supported by the server. If not, it uses the LIST command and supports dozens of different listing formats.

    (I'm the author of WinSCP)