Upload files by FTP

Octopus.Script exported 2018-07-05 by Zhaph belongs to ‘Winscp’ category.

Upload files to a remote server via File Transfer Protocol (SFTP or FTP) using WinSCP.

This step template requires the WinSCP .NET Assembly to be installed on the server running the deployment.

Notes on usage

You will need to have a “Deploy a Package” step before this step to supply the files to deploy. You can have this all happen on your Octopus Server rather than one of your web servers by following the steps at Deploying Packages to your Octopus Server.

Cleaning up deployments

If you aren’t deploying to an application host and don’t want the deployment files to persist on the tentacle irrespective of the life cycle retention policy ensure that you set the “Delete Previous Deployment” to true and they will be removed.

Parameters

When steps based on the template are included in a project’s deployment process, the parameters below can be set.

Path to WinScp

PathToWinScp =

The directory where you extracted the WinSCP .NET Assembly.

Host

FtpHost =

The address of your FTP server. Example: ftp.yourhost.com.

Username

FtpUsername = anonymous

If no username is specified, the well-known username anonymous will be used.

Password

FtpPassword = guest

If no password is specified, the well-known password guest will be used.

If the password field is bound, the binding expression will be visible to other authorized users.

Host Key Fingerprint(s)

FtpHostKeyFingerprint =

By supplying a host key fingerprint (or a semicolon separated list), you will force the deployment into SFTP mode, and automatically accept the host certificates.

Passkey

FtpPasskey =

Path to the PPK passkey file, leave blank if using a Password

Passkey Phrase

FtpPasskeyPhrase =

If your passkey is encrypted, please supply the pass phrase.

If the password field is bound, the binding expression will be visible to other authorized users.

Remote directory

FtpRemoteDirectory =

The directory on your FTP server in which you want files to be placed. Example: /site/wwwroot

Package step name

FtpPackageStepName =

Name of the previously-deployed package step that contains the files that you want to deploy.

Delete unrecognized files

FtpDeleteUnrecognizedFiles = false

Files can exist on the FTP server that do not exist in the NuGet package. Examples may be binaries from a previous release, or uploaded images in a CMS. Use this option to choose how to treat these files.

Delete Deployment Step?

DeleteDeploymentStep = false

Should this script delete the deployment - for example, if you are running this on the Octopus Server, you might want to clean up the output of the previous package deploy step when this has completed.

Script body

Steps based on this template will execute the following PowerShell script.

## --------------------------------------------------------------------------------------
## Input
## --------------------------------------------------------------------------------------
$PathToWinScp = $OctopusParameters['PathToWinScp']
$FtpHost = $OctopusParameters['FtpHost']
$FtpUsername = $OctopusParameters['FtpUsername']
$FtpPassword = $OctopusParameters['FtpPassword']
$FtpHostKeyFingerprint = $OctopusParameters['FtpHostKeyFingerprint']
$FtpPasskey = $OctopusParameters['FtpPasskey']
$FtpPasskeyPhrase = $OctopusParameters['FtpPasskeyPhrase']
$FtpRemoteDirectory = $OctopusParameters['FtpRemoteDirectory']
$FtpPackageStepName = $OctopusParameters['FtpPackageStepName']
$FtpDeleteUnrecognizedFiles = $OctopusParameters['FtpDeleteUnrecognizedFiles']
$DeleteDeploymentStep = $OctopusParameters['DeleteDeploymentStep']

## --------------------------------------------------------------------------------------
## Helpers
## --------------------------------------------------------------------------------------
# Helper for validating input parameters
function Validate-Parameter([string]$foo, [string[]]$validInput, $parameterName) {
    if (! $parameterName -contains "Password")
    {
        Write-Host "${parameterName}: $foo"
    }
    if (! $foo) {
        throw "No value was set for $parameterName, and it cannot be empty"
    }
}

# A collection of functions that can be used by script steps to determine where packages installed
# by previous steps are located on the filesystem.
function Find-InstallLocations {
    $result = @()
    $OctopusParameters.Keys | foreach {
        if ($_.EndsWith('].Output.Package.InstallationDirectoryPath')) {
            $result += $OctopusParameters[$_]
        }
    }
    return $result
}

function Find-InstallLocation($stepName) {
    $result = $OctopusParameters.Keys | where {
        $_.Equals("Octopus.Action[$stepName].Output.Package.InstallationDirectoryPath",  [System.StringComparison]::OrdinalIgnoreCase)
    } | select -first 1

    if ($result) {
        return $OctopusParameters[$result]
    }

    throw "No install location found for step: $stepName"
}

function Find-SingleInstallLocation {
    $all = @(Find-InstallLocations)
    if ($all.Length -eq 1) {
        return $all[0]
    }
    if ($all.Length -eq 0) {
        throw "No package steps found"
    }
    throw "Multiple package steps have run; please specify a single step"
}

# Session.FileTransferred event handler
function FileTransferred
{
    param($e)

    if ($e.Error -eq $Null)
    {
        Write-Host ("Upload of {0} succeeded" -f $e.FileName)
    }
    else
    {
        Write-Error ("Upload of {0} failed: {1}" -f $e.FileName, $e.Error)
    }

    if ($e.Chmod -ne $Null)
    {
        if ($e.Chmod.Error -eq $Null)
        {
            Write-Host "##octopus[stdout-verbose]"
            Write-Host ("Permisions of {0} set to {1}" -f $e.Chmod.FileName, $e.Chmod.FilePermissions)
            Write-Host "##octopus[stdout-default]"
        }
        else
        {
            Write-Error ("Setting permissions of {0} failed: {1}" -f $e.Chmod.FileName, $e.Chmod.Error)
        }

    }
    else
    {
        Write-Host "##octopus[stdout-verbose]"
        Write-Host ("Permissions of {0} kept with their defaults" -f $e.Destination)
        Write-Host "##octopus[stdout-default]"
    }

    if ($e.Touch -ne $Null)
    {
        if ($e.Touch.Error -eq $Null)
        {
            Write-Host "##octopus[stdout-verbose]"
            Write-Host ("Timestamp of {0} set to {1}" -f $e.Touch.FileName, $e.Touch.LastWriteTime)
            Write-Host "##octopus[stdout-default]"
        }
        else
        {
            Write-Error ("Setting timestamp of {0} failed: {1}" -f $e.Touch.FileName, $e.Touch.Error)
        }

    }
    else
    {
        # This should never happen during "local to remote" synchronization
        Write-Host "##octopus[stdout-verbose]"
        Write-Host ("Timestamp of {0} kept with its default (current time)" -f $e.Destination)
        Write-Host "##octopus[stdout-default]"
    }
}

## --------------------------------------------------------------------------------------
## Configuration
## --------------------------------------------------------------------------------------
Validate-Parameter $PathToWinScp -parameterName "Path to WinSCP .NET Assembly"
Validate-Parameter $FtpHost -parameterName "Host"
Validate-Parameter $FtpUsername -parameterName "Username"
Validate-Parameter $FtpPassword -parameterName "Password"
Validate-Parameter $FtpRemoteDirectory -parameterName "Remote directory"
Validate-Parameter $FtpPackageStepName -parameterName "Package step name"
Validate-Parameter $FtpDeleteUnrecognizedFiles -parameterName "Delete unrecognized files"

## --------------------------------------------------------------------------------------
## Main script
## --------------------------------------------------------------------------------------

# Load WinSCP .NET assembly
$fullPathToWinScp = "$PathToWinScp\WinSCPnet.dll"
if(-not (Test-Path $fullPathToWinScp))
{
    throw "$PathToWinScp does not contain the WinSCP .NET Assembly"
}
Add-Type -Path $fullPathToWinScp

$stepPath = ""
if (-not [string]::IsNullOrEmpty($FtpPackageStepName)) {
    Write-Host "Finding path to package step: $FtpPackageStepName"
    $stepPath = Find-InstallLocation $FtpPackageStepName
} else {
    $stepPath = Find-SingleInstallLocation
}
Write-Host "Package was installed to: $stepPath"

try
{
    $sessionOptions = New-Object WinSCP.SessionOptions

    # WinSCP defaults to SFTP, but it's good to ensure that's the case
    if ($FtpHostKeyFingerprint -ne "") {
      $sessionOptions.Protocol = [WinScp.Protocol]::Sftp
    }
    else {
      $sessionOptions.Protocol = [WinSCP.Protocol]::Ftp
    }
    $sessionOptions.HostName = $FtpHost
    $sessionOptions.UserName = $FtpUsername

    $sessionOptions.SshHostKeyFingerprint = $FtpHostKeyFingerprint

    # If there is a path to the private key, use that instead of a password
    if ($FtpPasskey -ne "") {
      Write-Host "Attempting to use passkey instead of password"

      # Check key exists
      if (!(Test-Path $FtpPasskey)) {
        throw "Unable to locate passkey at: $FtpPasskey"
      }

      $sessionOptions.SshPrivateKeyPath = $FtpPasskey

      # If the key requires a passphrase to access
      if ($FtpPasskeyPhrase -ne "") {
        $sessionOptions.PrivateKeyPassphrase = $FtpPasskeyPhrase
      }
    }
    else {
      $sessionOptions.Password = $FtpPassword
    }

    $session = New-Object WinSCP.Session
    try
    {
        # Will continuously report progress of synchronization
        $session.add_FileTransferred( { FileTransferred($_) } )

        # Connect
        $session.Open($sessionOptions)

        Write-Host "Beginning synchronization between $stepPath and $FtpRemoteDirectory on $FtpHost"

        if (-not $session.FileExists($FtpRemoteDirectory))
        {
            Write-Host "Remote directory not found, creating $FtpRemoteDirectory"
            $session.CreateDirectory($FtpRemoteDirectory);
        }

        # Synchronize files
        $synchronizationResult = $session.SynchronizeDirectories(
            [WinSCP.SynchronizationMode]::Remote, $stepPath, $FtpRemoteDirectory, $FtpDeleteUnrecognizedFiles)

        # Throw on any error
        $synchronizationResult.Check()
    }
    finally
    {
        # Disconnect, clean up
        $session.Dispose()

        if ($DeleteDeploymentStep) {
          Remove-Item -Path $stepPath -Recurse
        }
    }

    exit 0
}
catch [Exception]
{
    throw $_.Exception.Message
}

Provided under the Apache License version 2.0.

Report an issue

To use this template in Octopus Deploy, copy the JSON below and paste it into the Library → Step templates → Import dialog.

{
  "Id": "3b534e57-e8b0-4a06-aa2c-9e7eba1f4337",
  "Name": "Upload files by FTP",
  "Description": "Upload files to a remote server via File Transfer Protocol (SFTP or FTP) using WinSCP.\n\nThis step template requires the [WinSCP .NET Assembly](http://winscp.net/eng/docs/library#downloading_and_installing_the_assembly) to be installed on the server running the deployment.\n\n# Notes on usage\n\nYou will need to have a \"Deploy a Package\" step before this step to supply the files to deploy. You can have this all happen on your Octopus Server rather than one of your web servers by following the steps at [Deploying Packages to your Octopus Server](https://octopus.com/docs/deployment-process/steps/how-to-run-steps-on-the-octopus-server#deploying-packages-to-your-octopus-server).\n\n## Cleaning up deployments\n\nIf you aren't deploying to an application host and don't want the deployment files to persist on the tentacle irrespective of the life cycle retention policy ensure that you set the \"Delete Previous Deployment\" to `true` and they will be removed.",
  "Version": 5,
  "ExportedAt": "2018-07-05T15:12:39.219Z",
  "ActionType": "Octopus.Script",
  "Author": "Zhaph",
  "Parameters": [
    {
      "Name": "PathToWinScp",
      "Label": "Path to WinScp",
      "HelpText": "The directory where you extracted the WinSCP .NET Assembly.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Name": "FtpHost",
      "Label": "Host",
      "HelpText": "The address of your FTP server. Example: `ftp.yourhost.com`.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Name": "FtpUsername",
      "Label": "Username",
      "HelpText": "If no username is specified, the well-known username `anonymous` will be used.",
      "DefaultValue": "anonymous",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Name": "FtpPassword",
      "Label": "Password",
      "HelpText": "If no password is specified, the well-known password `guest` will be used.\n\nIf the password field is bound, the binding expression will be visible to other authorized users.",
      "DefaultValue": "guest",
      "DisplaySettings": {
        "Octopus.ControlType": "Sensitive"
      }
    },
    {
      "Name": "FtpHostKeyFingerprint",
      "Label": "Host Key Fingerprint(s)",
      "HelpText": "By supplying a host key fingerprint (or a semicolon separated list), you will force the deployment into SFTP mode, and automatically accept the host certificates.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Name": "FtpPasskey",
      "Label": "Passkey",
      "HelpText": "Path to the PPK passkey file, leave blank if using a Password",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Name": "FtpPasskeyPhrase",
      "Label": "Passkey Phrase",
      "HelpText": "If your passkey is encrypted, please supply the pass phrase.\n\nIf the password field is bound, the binding expression will be visible to other authorized users.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "Sensitive"
      }
    },
    {
      "Name": "FtpRemoteDirectory",
      "Label": "Remote directory",
      "HelpText": "The directory on your FTP server in which you want files to be placed. Example: `/site/wwwroot`",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Name": "FtpPackageStepName",
      "Label": "Package step name",
      "HelpText": "Name of the previously-deployed package step that contains the files that you want to deploy.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "StepName"
      }
    },
    {
      "Name": "FtpDeleteUnrecognizedFiles",
      "Label": "Delete unrecognized files",
      "HelpText": "Files can exist on the FTP server that do not exist in the NuGet package. Examples may be binaries from a previous release, or uploaded images in a CMS. Use this option to choose how to treat these files.",
      "DefaultValue": "false",
      "DisplaySettings": {
        "Octopus.ControlType": "Checkbox"
      }
    },
    {
      "Name": "DeleteDeploymentStep",
      "Label": "Delete Deployment Step?",
      "HelpText": "Should this script delete the deployment - for example, if you are running this on the Octopus Server, you might want to clean up the output of the previous package deploy step when this has completed.",
      "DefaultValue": "false",
      "DisplaySettings": {
        "Octopus.ControlType": "Checkbox"
      }
    }
  ],
  "Properties": {
    "Octopus.Action.Script.Syntax": "PowerShell",
    "Octopus.Action.Script.ScriptBody": "## --------------------------------------------------------------------------------------\n## Input\n## --------------------------------------------------------------------------------------\n$PathToWinScp = $OctopusParameters['PathToWinScp']\n$FtpHost = $OctopusParameters['FtpHost']\n$FtpUsername = $OctopusParameters['FtpUsername']\n$FtpPassword = $OctopusParameters['FtpPassword']\n$FtpHostKeyFingerprint = $OctopusParameters['FtpHostKeyFingerprint']\n$FtpPasskey = $OctopusParameters['FtpPasskey']\n$FtpPasskeyPhrase = $OctopusParameters['FtpPasskeyPhrase']\n$FtpRemoteDirectory = $OctopusParameters['FtpRemoteDirectory']\n$FtpPackageStepName = $OctopusParameters['FtpPackageStepName']\n$FtpDeleteUnrecognizedFiles = $OctopusParameters['FtpDeleteUnrecognizedFiles']\n$DeleteDeploymentStep = $OctopusParameters['DeleteDeploymentStep']\n\n## --------------------------------------------------------------------------------------\n## Helpers\n## --------------------------------------------------------------------------------------\n# Helper for validating input parameters\nfunction Validate-Parameter([string]$foo, [string[]]$validInput, $parameterName) {\n    if (! $parameterName -contains \"Password\")\n    {\n        Write-Host \"${parameterName}: $foo\"\n    }\n    if (! $foo) {\n        throw \"No value was set for $parameterName, and it cannot be empty\"\n    }\n}\n\n# A collection of functions that can be used by script steps to determine where packages installed\n# by previous steps are located on the filesystem.\nfunction Find-InstallLocations {\n    $result = @()\n    $OctopusParameters.Keys | foreach {\n        if ($_.EndsWith('].Output.Package.InstallationDirectoryPath')) {\n            $result += $OctopusParameters[$_]\n        }\n    }\n    return $result\n}\n\nfunction Find-InstallLocation($stepName) {\n    $result = $OctopusParameters.Keys | where {\n        $_.Equals(\"Octopus.Action[$stepName].Output.Package.InstallationDirectoryPath\",  [System.StringComparison]::OrdinalIgnoreCase)\n    } | select -first 1\n\n    if ($result) {\n        return $OctopusParameters[$result]\n    }\n\n    throw \"No install location found for step: $stepName\"\n}\n\nfunction Find-SingleInstallLocation {\n    $all = @(Find-InstallLocations)\n    if ($all.Length -eq 1) {\n        return $all[0]\n    }\n    if ($all.Length -eq 0) {\n        throw \"No package steps found\"\n    }\n    throw \"Multiple package steps have run; please specify a single step\"\n}\n\n# Session.FileTransferred event handler\nfunction FileTransferred\n{\n    param($e)\n\n    if ($e.Error -eq $Null)\n    {\n        Write-Host (\"Upload of {0} succeeded\" -f $e.FileName)\n    }\n    else\n    {\n        Write-Error (\"Upload of {0} failed: {1}\" -f $e.FileName, $e.Error)\n    }\n\n    if ($e.Chmod -ne $Null)\n    {\n        if ($e.Chmod.Error -eq $Null)\n        {\n            Write-Host \"##octopus[stdout-verbose]\"\n            Write-Host (\"Permisions of {0} set to {1}\" -f $e.Chmod.FileName, $e.Chmod.FilePermissions)\n            Write-Host \"##octopus[stdout-default]\"\n        }\n        else\n        {\n            Write-Error (\"Setting permissions of {0} failed: {1}\" -f $e.Chmod.FileName, $e.Chmod.Error)\n        }\n\n    }\n    else\n    {\n        Write-Host \"##octopus[stdout-verbose]\"\n        Write-Host (\"Permissions of {0} kept with their defaults\" -f $e.Destination)\n        Write-Host \"##octopus[stdout-default]\"\n    }\n\n    if ($e.Touch -ne $Null)\n    {\n        if ($e.Touch.Error -eq $Null)\n        {\n            Write-Host \"##octopus[stdout-verbose]\"\n            Write-Host (\"Timestamp of {0} set to {1}\" -f $e.Touch.FileName, $e.Touch.LastWriteTime)\n            Write-Host \"##octopus[stdout-default]\"\n        }\n        else\n        {\n            Write-Error (\"Setting timestamp of {0} failed: {1}\" -f $e.Touch.FileName, $e.Touch.Error)\n        }\n\n    }\n    else\n    {\n        # This should never happen during \"local to remote\" synchronization\n        Write-Host \"##octopus[stdout-verbose]\"\n        Write-Host (\"Timestamp of {0} kept with its default (current time)\" -f $e.Destination)\n        Write-Host \"##octopus[stdout-default]\"\n    }\n}\n\n## --------------------------------------------------------------------------------------\n## Configuration\n## --------------------------------------------------------------------------------------\nValidate-Parameter $PathToWinScp -parameterName \"Path to WinSCP .NET Assembly\"\nValidate-Parameter $FtpHost -parameterName \"Host\"\nValidate-Parameter $FtpUsername -parameterName \"Username\"\nValidate-Parameter $FtpPassword -parameterName \"Password\"\nValidate-Parameter $FtpRemoteDirectory -parameterName \"Remote directory\"\nValidate-Parameter $FtpPackageStepName -parameterName \"Package step name\"\nValidate-Parameter $FtpDeleteUnrecognizedFiles -parameterName \"Delete unrecognized files\"\n\n## --------------------------------------------------------------------------------------\n## Main script\n## --------------------------------------------------------------------------------------\n\n# Load WinSCP .NET assembly\n$fullPathToWinScp = \"$PathToWinScp\\WinSCPnet.dll\"\nif(-not (Test-Path $fullPathToWinScp))\n{\n    throw \"$PathToWinScp does not contain the WinSCP .NET Assembly\"\n}\nAdd-Type -Path $fullPathToWinScp\n\n$stepPath = \"\"\nif (-not [string]::IsNullOrEmpty($FtpPackageStepName)) {\n    Write-Host \"Finding path to package step: $FtpPackageStepName\"\n    $stepPath = Find-InstallLocation $FtpPackageStepName\n} else {\n    $stepPath = Find-SingleInstallLocation\n}\nWrite-Host \"Package was installed to: $stepPath\"\n\ntry\n{\n    $sessionOptions = New-Object WinSCP.SessionOptions\n\n    # WinSCP defaults to SFTP, but it's good to ensure that's the case\n    if ($FtpHostKeyFingerprint -ne \"\") {\n      $sessionOptions.Protocol = [WinScp.Protocol]::Sftp\n    }\n    else {\n      $sessionOptions.Protocol = [WinSCP.Protocol]::Ftp\n    }\n    $sessionOptions.HostName = $FtpHost\n    $sessionOptions.UserName = $FtpUsername\n\n    $sessionOptions.SshHostKeyFingerprint = $FtpHostKeyFingerprint\n\n    # If there is a path to the private key, use that instead of a password\n    if ($FtpPasskey -ne \"\") {\n      Write-Host \"Attempting to use passkey instead of password\"\n\n      # Check key exists\n      if (!(Test-Path $FtpPasskey)) {\n        throw \"Unable to locate passkey at: $FtpPasskey\"\n      }\n\n      $sessionOptions.SshPrivateKeyPath = $FtpPasskey\n\n      # If the key requires a passphrase to access\n      if ($FtpPasskeyPhrase -ne \"\") {\n        $sessionOptions.PrivateKeyPassphrase = $FtpPasskeyPhrase\n      }\n    }\n    else {\n      $sessionOptions.Password = $FtpPassword\n    }\n\n    $session = New-Object WinSCP.Session\n    try\n    {\n        # Will continuously report progress of synchronization\n        $session.add_FileTransferred( { FileTransferred($_) } )\n\n        # Connect\n        $session.Open($sessionOptions)\n\n        Write-Host \"Beginning synchronization between $stepPath and $FtpRemoteDirectory on $FtpHost\"\n\n        if (-not $session.FileExists($FtpRemoteDirectory))\n        {\n            Write-Host \"Remote directory not found, creating $FtpRemoteDirectory\"\n            $session.CreateDirectory($FtpRemoteDirectory);\n        }\n\n        # Synchronize files\n        $synchronizationResult = $session.SynchronizeDirectories(\n            [WinSCP.SynchronizationMode]::Remote, $stepPath, $FtpRemoteDirectory, $FtpDeleteUnrecognizedFiles)\n\n        # Throw on any error\n        $synchronizationResult.Check()\n    }\n    finally\n    {\n        # Disconnect, clean up\n        $session.Dispose()\n\n        if ($DeleteDeploymentStep) {\n          Remove-Item -Path $stepPath -Recurse\n        }\n    }\n\n    exit 0\n}\ncatch [Exception]\n{\n    throw $_.Exception.Message\n}\n",
    "Octopus.Action.Script.ScriptSource": "Inline"
  },
  "Category": "Winscp",
  "HistoryUrl": "https://github.com/OctopusDeploy/Library/commits/master/step-templates//opt/buildagent/work/75443764cd38076d/step-templates/ftp-uploadfiles.json",
  "Website": "/step-templates/3b534e57-e8b0-4a06-aa2c-9e7eba1f4337",
  "Logo": "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",
  "$Meta": {
    "Type": "ActionTemplate"
  }
}

History

Page updated on Thursday, July 5, 2018