diff --git a/CloudFtpBridge.sln b/CloudFtpBridge.sln index 9fe768e..6c970d4 100644 --- a/CloudFtpBridge.sln +++ b/CloudFtpBridge.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30717.126 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31912.275 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CA2FA16E-EF5A-42C1-90F4-0A9351CE1A22}" EndProject @@ -25,9 +25,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudFtpBridge.Infrastructure.Smtp", "src\CloudFtpBridge.Infrastructure.Smtp\CloudFtpBridge.Infrastructure.Smtp.csproj", "{AB3C17C0-1FB9-4EB1-9B61-ED55646E4B4B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudFtpBridge.Infrastructure.Smtp", "src\CloudFtpBridge.Infrastructure.Smtp\CloudFtpBridge.Infrastructure.Smtp.csproj", "{AB3C17C0-1FB9-4EB1-9B61-ED55646E4B4B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudFtpBridge.Infrastructure.Json", "src\CloudFtpBridge.Infrastructure.Json\CloudFtpBridge.Infrastructure.Json.csproj", "{A6D99988-DB0B-4DCC-9A81-08A802526FFC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudFtpBridge.Infrastructure.Json", "src\CloudFtpBridge.Infrastructure.Json\CloudFtpBridge.Infrastructure.Json.csproj", "{A6D99988-DB0B-4DCC-9A81-08A802526FFC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudFtpBridge.Infrastructure.FTP", "src\CloudFtpBridge.Infrastructure.FTP\CloudFtpBridge.Infrastructure.FTP.csproj", "{0E9CF9DA-7F24-4CEE-9AA7-49AD78ACB75F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -63,6 +65,10 @@ Global {A6D99988-DB0B-4DCC-9A81-08A802526FFC}.Debug|Any CPU.Build.0 = Debug|Any CPU {A6D99988-DB0B-4DCC-9A81-08A802526FFC}.Release|Any CPU.ActiveCfg = Release|Any CPU {A6D99988-DB0B-4DCC-9A81-08A802526FFC}.Release|Any CPU.Build.0 = Release|Any CPU + {0E9CF9DA-7F24-4CEE-9AA7-49AD78ACB75F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E9CF9DA-7F24-4CEE-9AA7-49AD78ACB75F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E9CF9DA-7F24-4CEE-9AA7-49AD78ACB75F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E9CF9DA-7F24-4CEE-9AA7-49AD78ACB75F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -76,6 +82,7 @@ Global {9BA84E2D-1E91-4F85-854B-90DF0BC5461E} = {CA2FA16E-EF5A-42C1-90F4-0A9351CE1A22} {AB3C17C0-1FB9-4EB1-9B61-ED55646E4B4B} = {C27977BE-7208-4B2A-8DC9-47EE43386957} {A6D99988-DB0B-4DCC-9A81-08A802526FFC} = {C27977BE-7208-4B2A-8DC9-47EE43386957} + {0E9CF9DA-7F24-4CEE-9AA7-49AD78ACB75F} = {C27977BE-7208-4B2A-8DC9-47EE43386957} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B3E20D95-3A68-4E34-ADCB-83BD3A715038} diff --git a/src/CloudFtpBridge.BlazorApp/CloudFtpBridge.BlazorApp.csproj b/src/CloudFtpBridge.BlazorApp/CloudFtpBridge.BlazorApp.csproj index ea6504d..e8c8b8a 100644 --- a/src/CloudFtpBridge.BlazorApp/CloudFtpBridge.BlazorApp.csproj +++ b/src/CloudFtpBridge.BlazorApp/CloudFtpBridge.BlazorApp.csproj @@ -25,6 +25,7 @@ + diff --git a/src/CloudFtpBridge.BlazorApp/Pages/Workflows/WorkflowDetailPage.razor b/src/CloudFtpBridge.BlazorApp/Pages/Workflows/WorkflowDetailPage.razor index e23f2e3..de2a1f8 100644 --- a/src/CloudFtpBridge.BlazorApp/Pages/Workflows/WorkflowDetailPage.razor +++ b/src/CloudFtpBridge.BlazorApp/Pages/Workflows/WorkflowDetailPage.razor @@ -30,7 +30,8 @@
- + + The source location where files will be transferred FROM. @@ -72,6 +73,9 @@ case SupportedFileSystems.FluentFTP: break; + case SupportedFileSystems.FTP: + + break; case SupportedFileSystems.Local: break; @@ -85,7 +89,8 @@
- + + The destination location where files will be transferred TO. @@ -108,6 +113,9 @@ case SupportedFileSystems.FluentFTP: break; + case SupportedFileSystems.FTP: + + break; case SupportedFileSystems.Local: break; diff --git a/src/CloudFtpBridge.BlazorApp/Program.cs b/src/CloudFtpBridge.BlazorApp/Program.cs index c767660..0999683 100644 --- a/src/CloudFtpBridge.BlazorApp/Program.cs +++ b/src/CloudFtpBridge.BlazorApp/Program.cs @@ -57,6 +57,7 @@ public static IHostBuilder CreateHostBuilder(string[] args) => app.UseFluentFTPFileSystem(); app.UseLocalFileSystem(); + app.UseFTPFileSystem(); }); services.AddHostedService(); diff --git a/src/CloudFtpBridge.BlazorApp/Shared/FileSystemOptions/FTPOptions.razor b/src/CloudFtpBridge.BlazorApp/Shared/FileSystemOptions/FTPOptions.razor new file mode 100644 index 0000000..c224b05 --- /dev/null +++ b/src/CloudFtpBridge.BlazorApp/Shared/FileSystemOptions/FTPOptions.razor @@ -0,0 +1,143 @@ +@using CloudFtpBridge.Infrastructure.FTP + +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + + The full path to the directory. Usually starts with a forward slash (/). +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + + When enabled, FTP will utilize passive connectivity. +
+
+
+ +
+
+
+ + +
+
+
+ +@if (!AutoConnect) +{ +
+
+
+ + + + + +
+
+
+} + +@code +{ + public const string ConfigPrefix = "CloudFtpBridge:Infrastructure:FTP:"; + + [Parameter] + public Dictionary Options { get; set; } = new Dictionary(); + + protected string ComponentId { get; } = Guid.NewGuid().ToString(); + + protected bool AutoConnect + { + get + { + var value = Get("AutoConnect"); + + return string.IsNullOrWhiteSpace(value) ? false : bool.Parse(value); + } + + set => Set("AutoConnect", value); + } + + protected bool UseFtps + { + get + { + var value = Get("UseFtps"); + + return string.IsNullOrWhiteSpace(value) ? false : bool.Parse(value); + } + + set => Set("UseFtps", value); + } + + protected string DataConnectionType + { + get => Get("DataConnectionType"); + set => Set("DataConnectionType", value); + } + + protected string Get(string propertyName) + { + var key = $"{ConfigPrefix}{propertyName}"; + + if (!Options.ContainsKey(key)) + { + return string.Empty; + } + + return Options[key]; + } + + protected void Set(string propertyName, object value) + { + var key = $"{ConfigPrefix}{propertyName}"; + + Options[key] = value?.ToString(); + } + + protected override void OnParametersSet() + { + if (!Options.Any()) + { + // set defaults for new workflows + new FTPFileSystemOptions().ToStringDictionary(ConfigPrefix, Options); + } + } +} diff --git a/src/CloudFtpBridge.BlazorApp/Shared/FileSystemOptions/SupportedFileSystems.cs b/src/CloudFtpBridge.BlazorApp/Shared/FileSystemOptions/SupportedFileSystems.cs index d6e9fce..e33a6bf 100644 --- a/src/CloudFtpBridge.BlazorApp/Shared/FileSystemOptions/SupportedFileSystems.cs +++ b/src/CloudFtpBridge.BlazorApp/Shared/FileSystemOptions/SupportedFileSystems.cs @@ -3,6 +3,7 @@ public static class SupportedFileSystems { public const string FluentFTP = "CloudFtpBridge.Infrastructure.FluentFTP.FluentFTPFileSystem"; + public const string FTP = "CloudFtpBridge.Infrastructure.FTP.FTPFileSystem"; public const string Local = "CloudFtpBridge.Infrastructure.LocalFileSystem.LocalFileSystem"; } } diff --git a/src/CloudFtpBridge.Core/Services/FileSystemActivator.cs b/src/CloudFtpBridge.Core/Services/FileSystemActivator.cs index 2d6eeb8..901f0b3 100644 --- a/src/CloudFtpBridge.Core/Services/FileSystemActivator.cs +++ b/src/CloudFtpBridge.Core/Services/FileSystemActivator.cs @@ -30,6 +30,7 @@ public IFileSystem Activate(string fileSystemTypeName, IDictionary !t.IsInterface && typeof(IFileSystem).IsAssignableFrom(t)) .FirstOrDefault(t => t.FullName.Equals(fileSystemTypeName)); + if (fileSystemType == null) { _logger.LogError("No file system implementation could be found matching the name {FileSystemType}.", fileSystemTypeName); diff --git a/src/CloudFtpBridge.Infrastructure.FTP/CloudFtpBridge.Infrastructure.FTP.csproj b/src/CloudFtpBridge.Infrastructure.FTP/CloudFtpBridge.Infrastructure.FTP.csproj new file mode 100644 index 0000000..9cd1250 --- /dev/null +++ b/src/CloudFtpBridge.Infrastructure.FTP/CloudFtpBridge.Infrastructure.FTP.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/CloudFtpBridge.Infrastructure.FTP/CloudFtpBridgeAppBuilderExtension.cs b/src/CloudFtpBridge.Infrastructure.FTP/CloudFtpBridgeAppBuilderExtension.cs new file mode 100644 index 0000000..58c68bc --- /dev/null +++ b/src/CloudFtpBridge.Infrastructure.FTP/CloudFtpBridgeAppBuilderExtension.cs @@ -0,0 +1,13 @@ +using CloudFtpBridge.Infrastructure.FTP; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class CloudFtpBridgeAppBuilderExtensions + { + public static CloudFtpBridgeAppBuilder UseFTPFileSystem(this CloudFtpBridgeAppBuilder builder) + { + builder.AddTransient(); + return builder; + } + } + } diff --git a/src/CloudFtpBridge.Infrastructure.FTP/FTPFileSystem.cs b/src/CloudFtpBridge.Infrastructure.FTP/FTPFileSystem.cs new file mode 100644 index 0000000..6dfa7f5 --- /dev/null +++ b/src/CloudFtpBridge.Infrastructure.FTP/FTPFileSystem.cs @@ -0,0 +1,85 @@ +using CloudFtpBridge.Core.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CloudFtpBridge.Core.Models; +using CloudFtpBridge.Core.Utils; + +namespace CloudFtpBridge.Infrastructure.FTP +{ + public class FTPFileSystem : IFileSystem + { + private readonly FTPFileSystemOptions _options = new FTPFileSystemOptions(); + private readonly FtpWebRequestClient _ftpClient; + + public FTPFileSystem( + IConfiguration configuration, + ILogger logger) + { + configuration.GetSection("CloudFtpBridge:Infrastructure:FTP").Bind(_options); + bool passive = _options.AutoConnect || _options.DataConnectionType.Equals("Passive"); + + _ftpClient = new FtpWebRequestClient(_options.Host, _options.Path, _options.Port, _options.Username, + _options.Password, logger, passive, _options.UseFtps); + } + + public async Task Delete(string fileName) + { + await Task.Run(() =>_ftpClient.Delete(fileName)); + } + + public async Task HasFiles() + { + var names = await Task.Run(()=> _ftpClient.GetListOfFiles()); + return names.Count > 0; + } + + public async Task> List() + { + var items = await Task.Run(() => _ftpClient.GetListOfFiles()); + return items + .Select(i => new FileRef(i)) + .ToArray(); + } + + public async Task Read(string fileName) + { + var memStream = new MemoryStream(); + + var stream = await Task.Run(() => _ftpClient.GetFile(fileName)); + + if (stream != null) + { + await stream.CopyToAsync(memStream); + memStream.Seek(0, SeekOrigin.Begin); + } + + return memStream; + } + + public async Task Rename(string oldFileName, string newFileName, bool overwriteExisting) + { + if (!overwriteExisting && await Task.Run(()=>_ftpClient.FileExists(newFileName))) + { + throw new InvalidOperationException($"Unable to rename {oldFileName}. The file {newFileName} already exists."); + } + + await Task.Run(()=>_ftpClient.Rename(oldFileName, newFileName)); + } + + public async Task Write(string fileName, Stream fromStream) + { + if (fromStream.CanSeek) + { + fromStream.Seek(0, SeekOrigin.Begin); + } + + await Task.Run(()=>_ftpClient.SendToFtp(fileName, fromStream)); + } + } +} diff --git a/src/CloudFtpBridge.Infrastructure.FTP/FTPFileSystemOptions.cs b/src/CloudFtpBridge.Infrastructure.FTP/FTPFileSystemOptions.cs new file mode 100644 index 0000000..939ed0a --- /dev/null +++ b/src/CloudFtpBridge.Infrastructure.FTP/FTPFileSystemOptions.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CloudFtpBridge.Infrastructure.FTP +{ + public class FTPFileSystemOptions + { + private const string _ConfigPrefix = "CloudFtpBridge:Infrastructure:FTP:"; + + public FTPFileSystemOptions() + { } + + public FTPFileSystemOptions(IDictionary configuration) + { + configuration.ToObject(_ConfigPrefix, this); + } + + public bool AutoConnect { get; set; } = true; + public string DataConnectionType { get; set; } + public string Host { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + public int Port { get; set; } = 21; + public string Username { get; set; } = string.Empty; + public string LocalPath { get; set; } + public bool UseFtps { get; set; } = false; + + public Dictionary ToDictionary(Dictionary dictionary = null) + { + return this.ToStringDictionary(_ConfigPrefix, dictionary); + } + } +} diff --git a/src/CloudFtpBridge.Infrastructure.FTP/FtpWebRequestClient.cs b/src/CloudFtpBridge.Infrastructure.FTP/FtpWebRequestClient.cs new file mode 100644 index 0000000..9df2243 --- /dev/null +++ b/src/CloudFtpBridge.Infrastructure.FTP/FtpWebRequestClient.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using Microsoft.Extensions.Logging; + +namespace CloudFtpBridge.Infrastructure.FTP +{ + public class FtpWebRequestClient + { + public FtpWebRequestClient(string ftpUrl, string ftpPath, int ftpPort, string userName, string password, ILogger logger, + bool passiveFtp = false, bool useFtps = false) + { + FtpUrl = ftpUrl; + Dir = ftpPath; + Port = ftpPort; + User = userName; + Pass = password; + UsePassive = passiveFtp; + UseFtps = useFtps; + Logger = logger; + } + + public void SendToFtp(string fileName, Stream fromStream) + { + bool fileExist = false; + + if (!FilesOnFtpCaptured) + FilesOnFtp = GetListOfFiles(); + + MemoryStream memStream = new MemoryStream(); + fromStream.CopyTo(memStream); + + FtpWebRequest ftpReq = CreateRequest(fileName); + + //Check if file exists on FTP if so set fileExist to true + if (FilesOnFtp != null && FilesOnFtp.Count >0) + { + foreach (string f in FilesOnFtp) + { + if (fileName == f) + { + fileExist = true; + Logger.LogInformation("FtpWebRequest: File exists on FTP server. We will append to the file."); + } + } + } + + ftpReq.Method = fileExist ? WebRequestMethods.Ftp.AppendFile : WebRequestMethods.Ftp.UploadFile; + ftpReq.Credentials = new NetworkCredential(User, Pass); + + byte[] fileContents = memStream.ToArray(); + ftpReq.ContentLength = fileContents.Length; + + Stream requestStream = ftpReq.GetRequestStream(); + requestStream.Write(fileContents, 0, fileContents.Length); + requestStream.Close(); + + FtpWebResponse response = (FtpWebResponse)ftpReq.GetResponse(); + response.Close(); + + Logger.LogInformation("FtpWebRequest: Upload was successful: " + fileName); + } + public void Delete(string fileName) + { + try + { + Logger.LogInformation("FtpWebRequest: Attempting Delete for file: " + fileName); + var reqFtp = CreateRequest(fileName); + reqFtp.Method = WebRequestMethods.Ftp.DeleteFile; + var respDel = (FtpWebResponse)reqFtp.GetResponse(); + respDel.Close(); + + Logger.LogInformation("FtpWebRequest: Delete Successful"); + } + catch (Exception x) + { + Logger.LogError("FtpWebRequest: Security, Permissions, or VAN moves file. Could not delete file from remote FTP server. File name: " + fileName+" Full exception: " + x.Message); + } + } + + public List GetListOfFiles() + { + List files = new List(); + WebResponse response = null; + StreamReader reader = null; + try + { + var reqFtp = CreateRequest(); + reqFtp.Method = WebRequestMethods.Ftp.ListDirectory; + response = reqFtp.GetResponse(); + + reader = new StreamReader(response.GetResponseStream()); + + string file = reader.ReadLine(); + while (file != null) + { + files.Add(file); + file = reader.ReadLine(); + } + + reader.Close(); + response.Close(); + FilesOnFtpCaptured = true; + } + catch (Exception ex) + { + reader?.Close(); + response?.Close(); + Logger.LogError("FtpWebRequest: Exception in Getting Directory List: " + ex.Message); + } + + return files; + } + + private FtpWebRequest CreateRequest(string fileName = "") + { + var reqFtp = (FtpWebRequest)WebRequest.Create(new Uri(CreateUrl(fileName))); + reqFtp.UseBinary = false; + reqFtp.Credentials = new NetworkCredential(User, Pass); + reqFtp.Proxy = null; + reqFtp.KeepAlive = true; + reqFtp.UsePassive = UsePassive; + reqFtp.Timeout = 600000; + reqFtp.EnableSsl = UseFtps; + + return reqFtp; + } + + private string CreateUrl(string fileName) + { + string path; + + var url = FtpUrl.EndsWith("/") ? FtpUrl.Remove(FtpUrl.Length - 1) : FtpUrl; + + if (Dir.StartsWith("/") || Dir.EndsWith("/")) + { + path = Dir; + + if (path.StartsWith("/")) + { + path = Dir.Substring(1); + } + + if (path.EndsWith("/")) + { + path = path.Remove(path.Length - 1); + } + } + else + { + path = Dir; + } + + return "ftp://" + url + ":" + Port.ToString() + "/" + path + "/" + fileName; + } + + public Stream GetFile(string file) + { + try + { + Logger.LogInformation("FtpWebRequest: Attempting to receive file: " + file); + string uri = CreateUrl(file); + + Uri ftpServerUri = new Uri(uri); + + if (ftpServerUri.Scheme != Uri.UriSchemeFtp) + { + return null; + } + + var reqFtp = CreateRequest(file); + reqFtp.Method = WebRequestMethods.Ftp.DownloadFile; + FtpWebResponse response = (FtpWebResponse)reqFtp.GetResponse(); + Stream responseStream = response.GetResponseStream(); + + Logger.LogInformation("FtpWebRequest: File has been received successfully."); + return responseStream; + } + catch (Exception ex) + { + Logger.LogError("FtpWebRequest: Get File failed Exception: " + ex.Message); + return null; + } + } + + public void Rename(string oldFileName, string newFileName) + { + try + { + Logger.LogInformation("FtpWebRequest: Attempting to rename file from "+ oldFileName +" to " + newFileName); + var reqFtp = CreateRequest(oldFileName); + reqFtp.Method = WebRequestMethods.Ftp.Rename; + reqFtp.RenameTo = newFileName; + FtpWebResponse response = (FtpWebResponse)reqFtp.GetResponse(); + Logger.LogInformation("FtpWebRequest: File rename successful"); + response.Close(); + } + catch (Exception e) + { + Logger.LogError("FtpWebRequest: Rename failed Exception: " + e.Message); + } + } + + public bool FileExists(string fileName) + { + if (!FilesOnFtpCaptured) + { + FilesOnFtp = GetListOfFiles(); + FilesOnFtpCaptured = true; + } + + return FilesOnFtp.Any(fileName.Equals); + } + + #region FTP Properties + public string FtpUrl { get; set; } + public string User { get; set; } + public string Pass { get; set; } + public string Dir { get; set; } + public string LocalDestDir { get; set; } = string.Empty; + public bool EnableArchive { get; set; } = false; + public bool UsePassive { get; set; } + public bool UseFtps { get; set; } + public int Port { get; set; } = 21; + public List FilesOnFtp { get; set; } + public bool FilesOnFtpCaptured { get; set; } + public ILogger Logger { get; set; } + + #endregion + } +}