PowerShell How can I delete a directory with FTP?

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:

  • List the remote directory
  • Iterate the entries, deleting files and recursing into subdirectories (listing them again, etc.)

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:

  • Do an operation on a file name that is certain to fail for a file and to succeed for directory (or vice versa). I.e. you can try to download the "name". If that succeeds, it's a file, if that fails, it's a directory. But that can become a performance problem, when you have a large number of entries.
  • You may be lucky and in your specific case, you can tell a file from a directory by a file name (i.e. all your files have an extension, while subdirectories do not)
  • You use a long directory listing (LIST command = ListDirectoryDetails method) and try to parse a server-specific listing. Many FTP servers use *nix-style listing, where you identify a directory by the d at the very beginning of the entry. But many servers use a different format. The following example uses this approach (assuming the *nix format).
  • In this specific case, you can just try to delete the entry as a file. If deleting fails, try to list the entry as a directory. If the listing succeeds, you assume it's a folder and proceed accordingly. Unfortunately some servers do not error, when you try to list a file. They will just return a listing with a single entry for the file.
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)