Skip to content

Commit

Permalink
Merge pull request #2656 from FoothillSolutions/port-ftp-module
Browse files Browse the repository at this point in the history
Porting FTP module to FAKE 5
  • Loading branch information
yazeedobaid authored Feb 10, 2022
2 parents e254a94 + 9318698 commit 96dc417
Show file tree
Hide file tree
Showing 11 changed files with 319 additions and 12 deletions.
15 changes: 15 additions & 0 deletions Fake.sln
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Net.Http", "src\app\Fa
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Net.SSH", "src\app\Fake.Net.SSH\Fake.Net.SSH.fsproj", "{5B2A7546-A441-45C9-8176-2872E2A30477}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Net.FTP", "src\app\Fake.Net.FTP\Fake.Net.FTP.fsproj", "{18C490E3-EA3F-4DC5-87A0-1A02309F8664}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CCAC5CAB-03C8-4C11-ADBE-A0D05F6A4F18}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Core.UnitTests", "src\test\Fake.Core.UnitTests\Fake.Core.UnitTests.fsproj", "{31A5759B-B562-43C0-A845-14EFA4091543}"
Expand Down Expand Up @@ -647,6 +649,18 @@ Global
{5B2A7546-A441-45C9-8176-2872E2A30477}.Release|x64.Build.0 = Release|Any CPU
{5B2A7546-A441-45C9-8176-2872E2A30477}.Release|x86.ActiveCfg = Release|Any CPU
{5B2A7546-A441-45C9-8176-2872E2A30477}.Release|x86.Build.0 = Release|Any CPU
{18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Debug|x64.ActiveCfg = Debug|Any CPU
{18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Debug|x64.Build.0 = Debug|Any CPU
{18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Debug|x86.ActiveCfg = Debug|Any CPU
{18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Debug|x86.Build.0 = Debug|Any CPU
{18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Release|Any CPU.Build.0 = Release|Any CPU
{18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Release|x64.ActiveCfg = Release|Any CPU
{18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Release|x64.Build.0 = Release|Any CPU
{18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Release|x86.ActiveCfg = Release|Any CPU
{18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Release|x86.Build.0 = Release|Any CPU
{31A5759B-B562-43C0-A845-14EFA4091543}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{31A5759B-B562-43C0-A845-14EFA4091543}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31A5759B-B562-43C0-A845-14EFA4091543}.Debug|x64.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -1276,6 +1290,7 @@ Global
{13C1F95D-2FAD-4890-BF94-0AE7CF9AB2FC} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
{D24CEE35-B6C0-4C92-AE18-E80F90B69974} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
{5B2A7546-A441-45C9-8176-2872E2A30477} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
{18C490E3-EA3F-4DC5-87A0-1A02309F8664} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
{31A5759B-B562-43C0-A845-14EFA4091543} = {CCAC5CAB-03C8-4C11-ADBE-A0D05F6A4F18}
{D8850C67-0542-427A-ABCB-92174EA42C95} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
{8D72BED1-BC02-4B23-A631-4849BD0FD3E1} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
Expand Down
1 change: 1 addition & 0 deletions build.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ let dotnetAssemblyInfos =
"Fake.JavaScript.TypeScript", "Running TypeScript compiler"
"Fake.Net.Http", "HTTP Client"
"Fake.Net.SSH", "SSH operations"
"Fake.Net.FTP", "FTP operations"
"Fake.netcore", "Command line tool"
"Fake.Runtime", "Core runtime features"
"Fake.Sql.DacPac", "Sql Server Data Tools DacPac operations (Obsolete: Use Fake.Sql.SqlPackage instead)"
Expand Down
3 changes: 2 additions & 1 deletion help/templates/template.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@
<div class="navbar-dropdown" class="navbar-item">
<a href="/net-http.html" class="navbar-item">Http</a>
<a href="/apidocs/v5/fake-net-ssh.html" class="navbar-item">SSH</a>
</div>
<a href="/apidocs/v5/fake-net-ftp.html" class="navbar-item">FTP</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a href="/apidocs/v5/index.html#Fake.Sql" class="navbar-link">Sql</a>
Expand Down
19 changes: 19 additions & 0 deletions src/app/Fake.Net.FTP/AssemblyInfo.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Auto-Generated by FAKE; do not edit
namespace System
open System.Reflection

[<assembly: AssemblyTitleAttribute("FAKE - F# Make FTP operations")>]
[<assembly: AssemblyProductAttribute("FAKE - F# Make")>]
[<assembly: AssemblyVersionAttribute("5.22.0")>]
[<assembly: AssemblyInformationalVersionAttribute("5.22.0-alpha001.local.3+2022-02-07-14-27")>]
[<assembly: AssemblyFileVersionAttribute("5.22.0")>]
[<assembly: AssemblyMetadataAttribute("BuildDate","2022-02-07")>]
do ()

module internal AssemblyVersionInformation =
let [<Literal>] AssemblyTitle = "FAKE - F# Make FTP operations"
let [<Literal>] AssemblyProduct = "FAKE - F# Make"
let [<Literal>] AssemblyVersion = "5.22.0"
let [<Literal>] AssemblyInformationalVersion = "5.22.0-alpha001.local.3+2022-02-07-14-27"
let [<Literal>] AssemblyFileVersion = "5.22.0"
let [<Literal>] AssemblyMetadata_BuildDate = "2022-02-07"
210 changes: 210 additions & 0 deletions src/app/Fake.Net.FTP/FTP.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
namespace Fake.Net

open System
open System.IO
open System.Net
open System.Text.RegularExpressions
open Fake.Core

[<RequireQualifiedAccess>]
/// Contains helpers which allow to upload a whole folder/specific file into a FTP Server.
/// Uses `Passive Mode` FTP and handles all files as binary (and not ASCII).
/// Assumes direct network connectivity to destination FTP server (not via a proxy).
/// Does not support FTPS and SFTP.
module FTP =

type FtpServerInfo =
{ Server : string
Request : FtpWebRequest }

/// Gets a connection to the FTP server
let getServerInfo (serverNameIp : string) (user : string) (password : string) ftpMethod =
let ftpRequest = (WebRequest.Create serverNameIp) :?> FtpWebRequest
ftpRequest.Credentials <- NetworkCredential(user, password)
ftpRequest.Method <- ftpMethod
{ Server = serverNameIp
Request = ftpRequest }

/// Writes given byte array into the given stream
let rec private writeChunkToReqStream (chunk : byte []) (requestStream : Stream) (br : BinaryReader) =
if chunk.Length <> 0 then
requestStream.Write(chunk, 0, chunk.Length)
writeChunkToReqStream (br.ReadBytes 1024) requestStream br

let inline private getSubstring (fromPos : int) (str : string) (toPos : int) = str.Substring(fromPos, toPos)
let inline private getLastSlashPosition (str : string) = str.LastIndexOf(@"/") + 1

let private charactersValidator (directoryName : string) =
let invalidChars = [ "<"; ">"; ":"; "\""; "/"; "\\"; "|"; "?"; "*" ]
not (List.exists directoryName.Contains invalidChars)

let private namesValidator (directoryName : string) =
let invalidNames =
[ "CON"; "PRN"; "AUX"; "NUL"; "COM1"; "COM2"; "COM3"; "COM4"; "COM5"; "COM6"; "COM7"; "COM8"; "COM9"; "LPT1";
"LPT2"; "LPT3"; "LPT4"; "LPT5"; "LPT6"; "LPT7"; "LPT8"; "LPT9" ]
not (List.exists (fun s -> s = directoryName.ToUpper()) invalidNames) &&
not (List.exists (fun s -> directoryName.ToUpper().StartsWith $"%s{s}.") invalidNames)

let private customValidator (directoryName : string) =
not (directoryName.EndsWith(" ")) &&
not (directoryName.EndsWith("."))

/// [omit]
///Partial validation for folder name, based on http://msdn.microsoft.com/en-us/library/aa365247.aspx
let isValidDirectoryName (directoryName : string) =
let validators = [
charactersValidator
namesValidator
customValidator
]
List.forall (fun validator -> directoryName |> validator) validators

/// Checks to see if the `ftp content` string contains the string `Given_Folder_Name`
let inline regexCheck folderName ftpContents = Regex.IsMatch(ftpContents, $@"\s+%s{folderName}\s+")

/// Gets the contents/listing of files and folders in a given FTP server folder
/// ## Parameters
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
/// - `dirPath` - The full name of folder whose content need to be listed
let getFtpDirContents (server : string) (user : string) (pwd : string) (dirPath : string) =
Trace.logfn $"Getting FTP dir contents for %s{dirPath}"
dirPath
|> fun d -> getServerInfo $"%s{server}/%s{d}" user pwd WebRequestMethods.Ftp.ListDirectoryDetails
|> fun si ->
use response = (si.Request.GetResponse() :?> FtpWebResponse)
use responseStream = response.GetResponseStream()
use reader = new StreamReader(responseStream)
reader.ReadToEnd()

/// Uploads a single file from local directory into remote FTP folder.
/// ## Parameters
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
/// - `destPath` - The full local file path that needs to be uploaded
/// - `srcPath` - The full path to file which needs to be created, including all its parent folders
let uploadAFile (server : string) (user : string) (pwd : string) (destPath : string) (srcPath : string) =
Trace.logfn $"Uploading %s{srcPath} to %s{destPath}"
let fl = FileInfo(srcPath)
if (fl.Length <> 0L) then
destPath
|> fun d -> getServerInfo $"%s{server}/%s{d}" user pwd WebRequestMethods.Ftp.UploadFile
|> fun si ->
use fs = new FileStream(srcPath, FileMode.Open, FileAccess.Read)
use br = new BinaryReader(fs, System.Text.UTF8Encoding())
use requestStream = si.Request.GetRequestStream()
writeChunkToReqStream (br.ReadBytes 1024) requestStream br

/// Given a folder name, will check if that folder is present at a given root directory of a FTP server.
/// ## Parameters
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
let private isFolderInDirectoryList server user pwd destPath folderName =
destPath
|> getLastSlashPosition
|> getSubstring 0 destPath
|> getFtpDirContents server user pwd
|> regexCheck folderName

/// Given a folder path, will check if that folder is present at a given root directory of a FTP server.
/// ## Parameters
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
/// - `destPath` - The full name of folder which needs to be checked for existence, including all its parent folders
let isFolderPresent server user pwd (destPath : string) =
destPath
|> getLastSlashPosition
|> destPath.Substring
|> isFolderInDirectoryList server user pwd destPath

/// Creates a matching folder in FTP folder, if not already present.
/// ## Parameters
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
/// - `destPath` - The full name of folder which needs to be created, including all its parent folders
let createAFolder (server : string) (user : string) (pwd : string) (destPath : string) =
Trace.logfn $"Creating folder %s{destPath}"
if not ((String.IsNullOrEmpty destPath) || (isFolderPresent server user pwd destPath)) then
destPath
|> fun d -> getServerInfo $"%s{server}/%s{d}" user pwd WebRequestMethods.Ftp.MakeDirectory
|> fun si ->
use response = (si.Request.GetResponse() :?> FtpWebResponse)
Trace.logfn $"Create folder status: %s{response.StatusDescription}"

/// Uploads a given local folder to a given root dir on a FTP server.
/// ## Parameters
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
/// - `srcPath` - The local server path from which files need to be uploaded
/// - `rootDir` - The remote root dir where files need to be uploaded, leave this as empty, if files need to be uploaded to root dir of FTP server
let rec uploadAFolder server user pwd (srcPath : string) (rootDir : string) =
Trace.logfn $"Uploading folder %s{srcPath}"
let dirInfo = DirectoryInfo(srcPath)
if dirInfo.Exists && isValidDirectoryName rootDir then
dirInfo.GetFileSystemInfos() |> Seq.iter (fun fsi -> upload server user pwd fsi rootDir)

and private upload server user pwd (fsi : FileSystemInfo) (rootDir : string) =
match fsi.GetType().ToString() with
| "System.IO.DirectoryInfo" ->
createAFolder server user pwd rootDir
createAFolder server user pwd $"%s{rootDir}/%s{fsi.Name}"
uploadAFolder server user pwd fsi.FullName $"%s{rootDir}/%s{fsi.Name}"
| "System.IO.FileInfo" -> uploadAFile server user pwd $"%s{rootDir}/%s{fsi.Name}" fsi.FullName
| _ -> Trace.logfn $"Unknown object found at %A{fsi}"

/// Deletes a single file from remote FTP folder.
/// ## Parameters
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
/// - `destPath` - The full path to the file which needs to be deleted, including all its parent folders
let deleteAFile (server : string) (user : string) (pwd : string) (destPath : string) =
Trace.logfn $"Deleting %s{destPath}"
destPath
|> fun p -> getServerInfo $"%s{server}/%s{p}" user pwd WebRequestMethods.Ftp.DeleteFile
|> fun si ->
use response = (si.Request.GetResponse() :?> FtpWebResponse)
Trace.logfn $"Delete file %s{destPath} status: %s{response.StatusDescription}"

let private getFolderContents (server : string) (user : string) (pwd : string) (destPath : string) =
getServerInfo $"%s{server}/%s{destPath}" user pwd WebRequestMethods.Ftp.ListDirectory
|> fun si ->
use response = (si.Request.GetResponse() :?> FtpWebResponse)
use responseStream = response.GetResponseStream()
use reader = new StreamReader(responseStream)
[ while not reader.EndOfStream do yield reader.ReadLine() ]

let private deleteEmptyFolder (server : string) (user : string) (pwd : string) (destPath : string) =
destPath
|> fun p -> getServerInfo $"%s{server}/%s{p}" user pwd WebRequestMethods.Ftp.RemoveDirectory
|> fun si ->
use response = (si.Request.GetResponse() :?> FtpWebResponse)
Trace.logfn $"Delete folder %s{destPath} status: %s{response.StatusDescription}"

/// Deletes a single folder from remote FTP folder.
/// ## Parameters
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
/// - `destPath` - The full path to the folder which needs to be deleted, including all its parent folders
let rec deleteAFolder (server : string) (user : string) (pwd : string) (destPath : string) =
Trace.logfn $"Deleting %s{destPath}"
let folderContents = getFolderContents server user pwd destPath

if folderContents |> List.isEmpty then
deleteEmptyFolder server user pwd destPath
else
folderContents
|> List.iter (fun entry ->
try
deleteAFile server user pwd (Path.Combine(destPath, entry))
with
| _ -> deleteAFolder server user pwd (Path.Combine(destPath, entry)))

deleteEmptyFolder server user pwd destPath
21 changes: 21 additions & 0 deletions src/app/Fake.Net.FTP/Fake.Net.FTP.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net472</TargetFrameworks>
<AssemblyName>Fake.Net.FTP</AssemblyName>
<OutputType>Library</OutputType>
</PropertyGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants)</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="FTP.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Fake.Core.Trace\Fake.Core.Trace.fsproj" />
</ItemGroup>
<Import Project="..\..\..\.paket\Paket.Restore.targets" />
</Project>
4 changes: 4 additions & 0 deletions src/app/Fake.Net.FTP/paket.references
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
group netcore

FSharp.Core
NETStandard.Library
3 changes: 3 additions & 0 deletions src/legacy/FakeLib/FakeLib.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@
<Compile Include="..\..\app\Fake.Net.SSH\SSH.fs">
<Link>Fake.Net.SSH/SSH.fs</Link>
</Compile>
<Compile Include="..\..\app\Fake.Net.FTP\FTP.fs">
<Link>Fake.Net.FTP/FTP.fs</Link>
</Compile>
<Compile Include="..\..\app\Fake.Core.Xml\Xml.fs">
<Link>Fake.Core.Xml/Xml.fs</Link>
</Compile>
Expand Down
Loading

0 comments on commit 96dc417

Please sign in to comment.