This commit is contained in:
Zeeshaun
2026-03-24 15:28:37 -05:00
parent 9cc87337b8
commit 73d62efb39
58 changed files with 2952 additions and 0 deletions
+330
View File
@@ -0,0 +1,330 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LCU.Api.Internal.FTPLib\LCU.Api.Internal.FTPLib.csproj" />
<ProjectReference Include="..\LCU.Api.Internal.FTP\LCU.Api.Internal.FTP.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="example.csv">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,46 @@
using System.IO;
using NUnit.Framework;
using Newtonsoft.Json;
using LCU.Api.Internal.FTPLib.Models.Enums;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Api.Internal.FTP.Services.FtpOpertionServices;
namespace LCU.Api.Internal.FTP.IntegrationTests;
public class SshFtpServiceTest
{
[SetUp]
public void Setup()
{
// Local docker SFTP Server setup:
// docker run -p 22:22 -d emberstack/sftp --name sftp -v /host/demo:/c/dev/sftp
}
/// <summary>
/// This test shows that we can use the SSH.NET sftp to easily transfer via byte array with no filesystem dependency
/// </summary>
[Test]
[Ignore("Can only be run locally for now.")]
public void TestSshFileRequest()
{
var request = new SshFtpRequest()
{
HostName = "127.0.0.1",
PortNumber = 22,
Password = "demo",
UserName = "demo",
Protocol = ProtocolType.Sftp,
RemoteFilePath = "test_uploaded1.csv",
LocalFileData = File.ReadAllBytes("example.csv")
};
var queueRequest = new QueueRequest()
{
Request = JsonConvert.SerializeObject(request),
FtpType = FtpType.SshFtp
};
var service = new SshFtpService();
var result = service.ExecuteAction(queueRequest);
Assert.True(result.IsSuccessful);
}
}
@@ -0,0 +1,4 @@
Name,Job Title,Address,State,City
John Doe,Designer,325 Pine Street,,Seattle
,,,,
Edward Green,Developer,110 Pike Street,WA,Seattle
1 Name Job Title Address State City
2 John Doe Designer 325 Pine Street Seattle
3
4 Edward Green Developer 110 Pike Street WA Seattle
@@ -0,0 +1,50 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29905.134
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.Api.Internal.FTP", "LCU.Api.Internal.FTP\LCU.Api.Internal.FTP.csproj", "{1C92A326-91A3-4904-B3EF-B766E10E7191}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.Api.Internal.FTPTests", "LCU.Api.Internal.FTPTests\LCU.Api.Internal.FTPTests.csproj", "{98503299-44F3-410A-B61F-36BC1706F132}"
ProjectSection(ProjectDependencies) = postProject
{1C92A326-91A3-4904-B3EF-B766E10E7191} = {1C92A326-91A3-4904-B3EF-B766E10E7191}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LCU.Api.Internal.FTPLib", "LCU.Api.Internal.FTPLib\LCU.Api.Internal.FTPLib.csproj", "{0D3255ED-449B-4F11-861B-5C9479520ED5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LCU.Api.Internal.FTP.IntegrationTests", "LCU.Api.Internal.FTP.IntegrationTests\LCU.Api.Internal.FTP.IntegrationTests.csproj", "{6CF2C6DE-B7FA-48B2-B720-208C0208B01E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1C92A326-91A3-4904-B3EF-B766E10E7191}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1C92A326-91A3-4904-B3EF-B766E10E7191}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1C92A326-91A3-4904-B3EF-B766E10E7191}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1C92A326-91A3-4904-B3EF-B766E10E7191}.Release|Any CPU.Build.0 = Release|Any CPU
{98503299-44F3-410A-B61F-36BC1706F132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98503299-44F3-410A-B61F-36BC1706F132}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98503299-44F3-410A-B61F-36BC1706F132}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98503299-44F3-410A-B61F-36BC1706F132}.Release|Any CPU.Build.0 = Release|Any CPU
{0D3255ED-449B-4F11-861B-5C9479520ED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D3255ED-449B-4F11-861B-5C9479520ED5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D3255ED-449B-4F11-861B-5C9479520ED5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D3255ED-449B-4F11-861B-5C9479520ED5}.Release|Any CPU.Build.0 = Release|Any CPU
{F5A17764-ED77-4DF1-923E-6DBEE6362F62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5A17764-ED77-4DF1-923E-6DBEE6362F62}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5A17764-ED77-4DF1-923E-6DBEE6362F62}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5A17764-ED77-4DF1-923E-6DBEE6362F62}.Release|Any CPU.Build.0 = Release|Any CPU
{6CF2C6DE-B7FA-48B2-B720-208C0208B01E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6CF2C6DE-B7FA-48B2-B720-208C0208B01E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6CF2C6DE-B7FA-48B2-B720-208C0208B01E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6CF2C6DE-B7FA-48B2-B720-208C0208B01E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {56A51B7E-A90A-4BFF-85D1-19ECB769AF6A}
EndGlobalSection
EndGlobal
@@ -0,0 +1,30 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
namespace LCU.Api.Internal.FTP.Configuration;
public class EnvironmentView : IEnvironmentView
{
public string Application => _env.ApplicationName;
public DateTime CurrentDate => DateTime.Now;
public string BuildVersion => Assembly.GetExecutingAssembly().GetName().Version.ToString();
public DateTime BuildDate => AssemblyBuildDate();
public string Environment => _env.EnvironmentName;
private readonly IWebHostEnvironment _env;
public EnvironmentView(IWebHostEnvironment env)
{
_env = env;
}
public static DateTime AssemblyBuildDate()
{
//https://stackoverflow.com/a/804310/266594
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
System.IO.FileInfo fileInfo = new System.IO.FileInfo(assembly.Location);
DateTime lastModified = fileInfo.LastWriteTime;
return lastModified;
}
}
@@ -0,0 +1,17 @@
namespace LCU.Api.Internal.FTP.Configuration;
public class FtpApiConfiguration
{
public int AttemptsLimit { get; set; }
public Apis Apis { get; set; }
}
public class Apis
{
public FileOperationApi FileOperationApi { get; set; }
}
public class FileOperationApi
{
public string BuildDynamicFileRequest { get; set; }
public string ValidateFileExists { get; set; }
}
@@ -0,0 +1,11 @@
using System;
namespace LCU.Api.Internal.FTP.Configuration;
public interface IEnvironmentView
{
string Application { get; }
DateTime CurrentDate { get; }
string BuildVersion { get; }
DateTime BuildDate { get; }
string Environment { get; }
}
@@ -0,0 +1,42 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using MassTransit;
using LCU.Api.Internal.FTP.Services;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Library.Internal.Exceptions;
using LCU.Api.Internal.MaidBotLib.Models;
namespace LCU.Api.Internal.FTP.Consumers;
public class SftpConsumer : IConsumer<QueueRequest>
{
private readonly IServiceScopeFactory _scopeFactory;
public SftpConsumer(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task Consume(ConsumeContext<QueueRequest> context)
{
try
{
using var scope = _scopeFactory.CreateScope();
await Task.Run(() => scope.ServiceProvider.GetRequiredService<IFtpService>().HandleFtpRequest(context.Message));
}
catch (ServiceException)
{
//Masstransit handles the exception. https://masstransit-project.com/usage/exceptions.html#retry
throw;
}
catch (Exception exception)
{
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment))
.Fatal(exception, $"Unexpected Failure");
//Masstransit handles the exception. https://masstransit-project.com/usage/exceptions.html#retry
throw;
}
}
}
@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using LCU.Api.Internal.FTP.Extensions;
using LCU.Api.Internal.FTP.Helpers;
using LCU.Api.Internal.FTP.Services;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Api.Internal.FTP.Services.FtpOpertionServices;
using LCU.Library.Internal.Exceptions;
namespace LCU.Api.Internal.FTP.Controllers;
[Route("api/[controller]")]
[ApiController]
public class SftpController : ControllerBase
{
private readonly IQueueFtpService _queueFtpService;
private readonly IWinScpListDirectoryService _winScpSftpListDirectoryService;
public SftpController(IQueueFtpService queueFtpService,
IWinScpListDirectoryService winScpSftpListDirectoryService)
{
_queueFtpService = queueFtpService;
_winScpSftpListDirectoryService = winScpSftpListDirectoryService;
}
[Obsolete("01/04/2022 - Please use QueueSftpUploadRequest Endpoint for Sftp Request and SshFtpController QueueSshFtpRequest Endpoint")]
[HttpPost]
[ProducesResponseType(201)]
[ProducesResponseType(typeof(List<string>), 400)]
[ProducesResponseType(500)]
public IActionResult Post([FromBody] SftpRequest request)
{
try
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState.GetErrorMessages());
}
if (!WinScpSessionOptionValidators.IsValidSessionOptions(request))
{
Log.Error("Sftp request can not be created into a valid session options.");
return BadRequest("Sftp request can not be created into a valid session options.");
}
_queueFtpService.QueueSftp(request);
return Ok();
}
catch (ServiceException)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[Obsolete("01/04/2022 - Please use QueueSftpUploadRequest Endpoint")]
[HttpPost("QueueSftpDynamicFileRequest")]
[ProducesResponseType(201)]
[ProducesResponseType(typeof(List<string>), 400)]
[ProducesResponseType(500)]
public IActionResult QueueSftpDynamicFileRequest([FromBody] SftpDynamicFileRequest request)
{
try
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState.GetErrorMessages());
}
if (!WinScpSessionOptionValidators.IsValidSessionOptions(request))
{
return BadRequest("Sftp request can not be created into a valid session options.");
}
_queueFtpService.QueueSftpDynamicFileRequest(request);
return Ok();
}
catch (ServiceException)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("QueueSftpUploadRequest")]
[ProducesResponseType(201)]
[ProducesResponseType(typeof(List<string>), 400)]
[ProducesResponseType(500)]
public IActionResult QueueSftpUploadRequest([FromBody] SftpUploadFileRequest request)
{
try
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState.GetErrorMessages());
}
_queueFtpService.QueueSftpUploadRequest(request);
return Ok();
}
catch (ServiceException)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("QueueSftpDownloadRequest")]
[ProducesResponseType(201)]
[ProducesResponseType(typeof(List<string>), 400)]
[ProducesResponseType(500)]
public IActionResult QueueSftpDownloadRequest([FromBody] SftpDownloadFileRequest request)
{
try
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState.GetErrorMessages());
}
_queueFtpService.QueueSftpDownloadRequest(request);
return Ok();
}
catch (ServiceException)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("ListFilesInDirectory")]
[ProducesResponseType(201)]
[ProducesResponseType(500)]
public IActionResult ListFilesInDirectory([FromQuery] ListDirectoryRequest request)
{
try
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState.GetErrorMessages());
}
var result = _winScpSftpListDirectoryService.GetListOfFilesInDirectory(request);
return Ok(result);
}
catch (ServiceException)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
@@ -0,0 +1,49 @@
using System;
using Serilog;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using LCU.Api.Internal.FTP.Extensions;
using LCU.Api.Internal.FTP.Services;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Library.Internal.Exceptions;
namespace LCU.Api.Internal.FTP.Controllers;
[Route("[controller]")]
[ApiController]
public class SshFtpController : ControllerBase
{
private readonly IQueueSshFtpService _queueSshFtpService;
public SshFtpController(IQueueSshFtpService queueSshFtpService)
{
_queueSshFtpService = queueSshFtpService;
}
[HttpPost("QueueSshFtpRequest")]
[ProducesResponseType(201)]
[ProducesResponseType(typeof(List<string>), 400)]
[ProducesResponseType(500)]
public IActionResult QueueSshFtpRequest([FromBody] SshFtpRequest request)
{
try
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState.GetErrorMessages());
}
_queueSshFtpService.QueueSshFtpRequest(request);
return Ok();
}
catch (ServiceException)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
@@ -0,0 +1,35 @@
using System;
using LCU.Api.Internal.FTP.Configuration;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Serilog;
namespace LCU.Api.Internal.FTP.Controllers;
[Route("api/[controller]")]
[ApiController]
public class StatusController : ControllerBase
{
private readonly IEnvironmentView _environmentView;
public StatusController(IEnvironmentView environmentView)
{
_environmentView = environmentView;
}
[HttpGet()]
[ProducesResponseType(200)]
[ProducesResponseType(500)]
public IActionResult Get()
{
try
{
return Ok(_environmentView);
}
catch (Exception ex)
{
Log.Error(ex, "Failure to return status");
return StatusCode((int)System.Net.HttpStatusCode.InternalServerError);
}
}
}
@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace LCU.Api.Internal.FTP.Extensions
{
public static class ModelStateMessages
{
public static List<string> GetErrorMessages(this ModelStateDictionary dictionary)
{
return dictionary.SelectMany(m => m.Value.Errors)
.Select(m => m.ErrorMessage)
.ToList();
}
}
}
@@ -0,0 +1,17 @@
using LCU.Api.Internal.FTPLib.Models.Enums;
using LCU.Api.Internal.FTPLib.Models;
namespace LCU.Api.Internal.FTP.Helpers;
public class FtpRequestResponseHelper
{
public static FtpRequestResponse ExceptionUpdateRequestResponse(FtpRequestResponse requestResponse,
FtpType ftpType,
string exception)
{
requestResponse.IsSuccessful = false;
requestResponse.MessageStack.Add($"Failure to execute ftp action. " +
$"FtpType: {ftpType}, " +
$"Exception: {exception} ");
return requestResponse;
}
}
@@ -0,0 +1,30 @@
using System;
using LCU.Api.Internal.FTPLib.Models;
namespace LCU.Api.Internal.FTP.Helpers;
[Obsolete("01/04/2022 - After the SftpController endpoints are no longer using this validation it can be removed. The model validation covers validation enough.")]
public static class WinScpSessionOptionValidators
{
public static bool IsValidSessionOptions(SftpRequest sftpRequest)
{
if (!IsValidHostInfo(sftpRequest) || !IsValidFileInfo(sftpRequest))
{
return false;
}
return WinScpSessionOptionsHelper.CreateSessionOptions(sftpRequest) != null;
}
private static bool IsValidFileInfo(SftpRequest sftpRequest)
{
return !string.IsNullOrWhiteSpace(sftpRequest.RemoteFilePath) &&
(!string.IsNullOrWhiteSpace(sftpRequest.LocalFilePath) || sftpRequest.LocalFileData != null);
}
private static bool IsValidHostInfo(SftpRequest sftpRequest)
{
return !string.IsNullOrWhiteSpace(sftpRequest.Password) &&
!string.IsNullOrWhiteSpace(sftpRequest.UserName) &&
!string.IsNullOrWhiteSpace(sftpRequest.HostName) && sftpRequest.PortNumber > 0;
}
}
@@ -0,0 +1,74 @@
using System;
using Serilog;
using WinSCP;
using LCU.Api.Internal.FTPLib.Models;
namespace LCU.Api.Internal.FTP.Helpers;
public static class WinScpSessionOptionsHelper
{
public static SessionOptions CreateSessionOptions(SftpRequest sftpRequest)
{
try
{
var protocol = FindWinScpProtocol(sftpRequest.Protocol);
SessionOptions sessionOptions = new SessionOptions
{
Protocol = protocol,
HostName = sftpRequest.HostName,
PortNumber = sftpRequest.PortNumber,
UserName = sftpRequest.UserName,
Password = sftpRequest.Password,
SshHostKeyFingerprint = sftpRequest.SshHostKeyFingerprint
};
return sessionOptions;
}
catch (Exception exception)
{
Log.Error(exception, "Error creating SessionOptions for Sftp Request");
return null;
}
}
public static SessionOptions CreateSessionOptions(SessionInformation sessionInformation)
{
try
{
var protocol = FindWinScpProtocol(sessionInformation.Protocol);
SessionOptions sessionOptions = new SessionOptions
{
Protocol = protocol,
HostName = sessionInformation.HostName,
PortNumber = sessionInformation.PortNumber,
UserName = sessionInformation.UserName,
Password = sessionInformation.Password,
SshHostKeyFingerprint = sessionInformation.SshHostKeyFingerprint
};
if (!string.IsNullOrWhiteSpace(sessionInformation.SshKeyFilePath))
{
sessionOptions.SshPrivateKeyPath = sessionInformation.SshKeyFilePath;
}
return sessionOptions;
}
catch (Exception exception)
{
Log.Error(exception, "Error creating SessionOptions for Sftp Request");
return null;
}
}
public static Protocol FindWinScpProtocol(ProtocolType protocolType)
{
switch (protocolType)
{
case ProtocolType.Sftp:
return Protocol.Sftp;
default:
return Protocol.Sftp;
}
}
}
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<PackageVersion>1.1.0</PackageVersion>
<PackageReleaseNotes>1.1.0 - Added GUID, byte[] upload, refactoring, tests</PackageReleaseNotes>
<Version>1.0.0</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DocumentationFile>C:\dev\projects\LCU.API.Internal.FTP\LCU.Api.Internal.FTP\LCU.Api.Internal.FTP.xml</DocumentationFile>
<NoWarn>1701;1702;1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DocumentationFile>C:\dev\projects\LCU.API.Internal.FTP\LCU.Api.Internal.FTP\LCU.Api.Internal.FTP.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LCU.Api.Internal.MaidBotLib" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.ApiResponse" Version="3.3.0" />
<PackageReference Include="LCU.Library.Internal.Exceptions" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.LcuMassTransit" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.LcuMassTransitModels" Version="1.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.10" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.Seq" Version="4.0.0" />
<PackageReference Include="SSH.NET" Version="2020.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
<PackageReference Include="WinSCP" Version="5.17.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LCU.Api.Internal.FTPLib\LCU.Api.Internal.FTPLib.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
</Project>
@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>LCU.Api.Internal.FTP</name>
</assembly>
<members>
</members>
</doc>
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Serilog;
namespace LCU.Api.Internal.FTP
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHost(args);
host.Run();
}
public static IWebHost CreateWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args).UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration
.ReadFrom.Configuration(hostingContext.Configuration))
.UseStartup<Startup>().Build();
}
}
@@ -0,0 +1,13 @@
namespace LCU.Api.Internal.FTP.Security
{
public class AzureActiveDirectoryOptions
{
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string Instance { get; set; }
public string Domain { get; set; }
public string TenantId { get; set; }
public string ValidAudience { get; set; }
}
}
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace LCU.Api.Internal.FTP.Security
{
public static class AzureAdServiceCollectionExtensions
{
public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder)
=> builder.AddAzureAdBearer(_ => { });
public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action<AzureActiveDirectoryOptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<JwtBearerOptions>, ConfigureAzureOptions>();
builder.AddJwtBearer();
return builder;
}
private class ConfigureAzureOptions : IConfigureNamedOptions<JwtBearerOptions>
{
private readonly AzureActiveDirectoryOptions _azureOptions;
public ConfigureAzureOptions(IOptions<AzureActiveDirectoryOptions> azureOptions)
{
_azureOptions = azureOptions.Value;
}
public void Configure(string name, JwtBearerOptions options)
{
options.Audience = _azureOptions.ClientId;
options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = _azureOptions.ValidAudience
};
}
public void Configure(JwtBearerOptions options)
{
Configure(Options.DefaultName, options);
}
}
}
}
@@ -0,0 +1,116 @@
using Newtonsoft.Json;
using Serilog;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using LCU.Library.Internal.FileOperationsModels.Models;
using LCU.Api.Internal.FTP.Configuration;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Library.Internal.Exceptions;
using System.Linq;
namespace LCU.Api.Internal.FTP.Services.FtpOpertionServices;
public class FileOperationsService : IFileOperationsService
{
private readonly IHttpClientFactory _httpFactory;
private readonly FtpApiConfiguration _ftpApiConfiguration;
public FileOperationsService(IHttpClientFactory httpFactory, FtpApiConfiguration ftpApiConfiguration)
{
_httpFactory = httpFactory;
_ftpApiConfiguration = ftpApiConfiguration;
}
public async Task<List<string>> GetListOfFilesInDirectory(SftpUploadFileRequest uploadRequest)
{
List<string> files = new List<string>();
try
{
var shouldBuildDynamicFileRequest = uploadRequest?.DynamicNameSetting?.RegexSettings != null && uploadRequest.DynamicNameSetting.RegexSettings.Any();
if (shouldBuildDynamicFileRequest)
{
files = await BuildDynamicFileRequest(uploadRequest);
}
else
{
files = await ValidateFileExists(uploadRequest);
}
}
catch (Exception ex)
{
Log.Error(ex, "Error Generating File List");
}
return files;
}
public async Task<List<string>> BuildDynamicFileRequest(SftpUploadFileRequest uploadRequest)
{
List<string> files = new List<string>();
try
{
var dynamicFileRequest = new DynamicFileListRequest()
{
Filename = uploadRequest.FinalLocalFileName,
DynamicFileSettings = uploadRequest.DynamicNameSetting,
Path = uploadRequest.FinalLocalFilePath.EndsWith("\\") ? uploadRequest.FinalLocalFilePath : $"{uploadRequest.FinalLocalFilePath}\\"
};
var encodedContent = JsonConvert.SerializeObject(dynamicFileRequest);
var content = new StringContent(encodedContent, Encoding.UTF8, "application/json");
var client = _httpFactory.CreateClient();
HttpResponseMessage response = await client.PostAsync(_ftpApiConfiguration.Apis.FileOperationApi.BuildDynamicFileRequest, content);
if (!response.IsSuccessStatusCode)
{
throw new ServiceException($"StatusCode:{response.StatusCode}, " +
$"Content: {response.Content.ReadAsStringAsync().Result}");
}
var jsonString = await response.Content.ReadAsStringAsync();
files = JsonConvert.DeserializeObject<List<string>>(jsonString);
}
catch (Exception ex)
{
Log.Error(ex, "Error Building Dynamic File List");
}
return files;
}
public async Task<List<string>> ValidateFileExists(SftpUploadFileRequest uploadRequest)
{
List<string> files = new List<string>();
try
{
var path = uploadRequest.FinalLocalFilePath.EndsWith("\\") ? uploadRequest.FinalLocalFilePath : $"{uploadRequest.FinalLocalFilePath}\\";
var finalFilePath = $"{path}{uploadRequest.FinalLocalFileName}";
var encodedContent = JsonConvert.SerializeObject(finalFilePath);
var content = new StringContent(encodedContent, Encoding.UTF8, "application/json");
var client = _httpFactory.CreateClient();
HttpResponseMessage response = await client.PostAsync(_ftpApiConfiguration.Apis.FileOperationApi.ValidateFileExists, content);
if (!response.IsSuccessStatusCode)
{
throw new ServiceException($"StatusCode:{response.StatusCode}, " +
$"Content: {response.Content.ReadAsStringAsync().Result}");
}
var jsonString = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<bool>(jsonString);
if (result)
{
files.Add(uploadRequest.FinalLocalFileName);
}
}
catch (Exception ex)
{
Log.Error(ex, "Error Validating if File Exist");
}
return files;
}
}
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.Api.Internal.FTPLib.Models;
namespace LCU.Api.Internal.FTP.Services.FtpOpertionServices
{
public interface IFileOperationsService
{
Task<List<string>> GetListOfFilesInDirectory(SftpUploadFileRequest uploadRequest);
}
}
@@ -0,0 +1,7 @@
using LCU.Api.Internal.FTPLib.Models;
namespace LCU.Api.Internal.FTP.Services.FtpOpertionServices;
public interface ISshFtpService
{
FtpRequestResponse ExecuteAction(QueueRequest queueRequest);
}
@@ -0,0 +1,8 @@
using LCU.Api.Internal.FTPLib.Models;
using System.Collections.Generic;
namespace LCU.Api.Internal.FTP.Services.FtpOpertionServices;
public interface IWinScpListDirectoryService
{
List<string> GetListOfFilesInDirectory(ListDirectoryRequest listDirectoryRequest);
}
@@ -0,0 +1,7 @@
using LCU.Api.Internal.FTPLib.Models;
namespace LCU.Api.Internal.FTP.Services.FtpOpertionServices;
public interface IWinScpSftpDownloadFilesService
{
FtpRequestResponse ExecuteAction(QueueRequest queueRequest);
}
@@ -0,0 +1,7 @@
using LCU.Api.Internal.FTPLib.Models;
namespace LCU.Api.Internal.FTP.Services.FtpOpertionServices;
public interface IWinScpSftpUploadFileService
{
FtpRequestResponse ExecuteAction(QueueRequest queueRequest);
}
@@ -0,0 +1,54 @@
using System;
using System.IO;
using Renci.SshNet;
using Serilog;
using Newtonsoft.Json;
using LCU.Api.Internal.FTPLib.Models;
namespace LCU.Api.Internal.FTP.Services.FtpOpertionServices;
public class SshFtpService : ISshFtpService
{
public FtpRequestResponse ExecuteAction(QueueRequest queueRequest)
{
var ftpRequestResponse = new FtpRequestResponse();
try
{
ArgumentNullException.ThrowIfNull(queueRequest);
var request = JsonConvert.DeserializeObject<SshFtpRequest>(queueRequest.Request);
ArgumentNullException.ThrowIfNull(request);
ftpRequestResponse.ReturnResponse = request;
TransferFiles(request);
}
catch (Exception exception)
{
ftpRequestResponse.IsSuccessful = false;
ftpRequestResponse.MessageStack.Add(exception.Message);
}
return ftpRequestResponse;
}
public void TransferFiles(SshFtpRequest request)
{
using var client = new SftpClient(request.HostName,
request.PortNumber == 0 ? 22 : request.PortNumber,
request.UserName, request.Password);
try
{
client.Connect();
using var stream = new MemoryStream(request.LocalFileData);
client.UploadFile(stream, request.RemoteFilePath);
Log.Information(
$"Uploaded [{request.RemoteFilePath}]");
}
catch (Exception exception)
{
throw new Exception($"Failed uploading [{request.RemoteFilePath}]" +
$"ExceptionMessage: {exception.Message}");
}
finally
{
client.Disconnect();
}
}
}
@@ -0,0 +1,57 @@
using System;
using Serilog;
using WinSCP;
using System.Collections.Generic;
using System.Linq;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Api.Internal.FTP.Helpers;
namespace LCU.Api.Internal.FTP.Services.FtpOpertionServices;
public class WinScpListDirectoryService : IWinScpListDirectoryService
{
private const char FileTypeFiles = '-';
public List<string> GetListOfFilesInDirectory(ListDirectoryRequest listDirectoryRequest)
{
try
{
var remoteDirectoryInfo = GetRemoteDirectoryInfo(listDirectoryRequest);
var fileNamesInDirectory = remoteDirectoryInfo.Files.Where(file => file.FileType == FileTypeFiles)
.Select(file => file.Name)
.ToList();
return fileNamesInDirectory;
}
catch (Exception exception)
{
Log.Error(exception, $"Failed to get directory list with WinSCP. " +
$"Host: {listDirectoryRequest.SessionInformation.HostName}, " +
$"RemotePath: {listDirectoryRequest.RemotePath}");
throw;
}
}
public RemoteDirectoryInfo GetRemoteDirectoryInfo(ListDirectoryRequest listDirectoryRequest)
{
RemoteDirectoryInfo remoteDirectoryInfo = null;
try
{
var sessionOptions = WinScpSessionOptionsHelper.CreateSessionOptions(listDirectoryRequest.SessionInformation);
using (var session = new Session())
{
session.Open(sessionOptions);
var transferOptions = new TransferOptions { TransferMode = TransferMode.Binary, PreserveTimestamp = false };
remoteDirectoryInfo = session.ListDirectory(listDirectoryRequest.RemotePath);
ArgumentNullException.ThrowIfNull(remoteDirectoryInfo);
}
return remoteDirectoryInfo;
}
catch (Exception exception)
{
Log.Error(exception, $"Failed to get directory list with WinSCP. " +
$"Host: {listDirectoryRequest.SessionInformation.HostName}, " +
$"RemotePath: {listDirectoryRequest.RemotePath}");
throw;
}
}
}
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Serilog;
using WinSCP;
using System.Text.RegularExpressions;
using LCU.Api.Internal.FTP.Helpers;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Library.Internal.FileOperationsModels.Shared;
namespace LCU.Api.Internal.FTP.Services.FtpOpertionServices;
public class WinScpSftpDownloadFilesService : IWinScpSftpDownloadFilesService
{
private readonly IWinScpListDirectoryService _winScpListDirectoryService;
public WinScpSftpDownloadFilesService(IWinScpListDirectoryService winScpListDirectoryService)
{
_winScpListDirectoryService = winScpListDirectoryService;
}
public FtpRequestResponse ExecuteAction(QueueRequest queueRequest)
{
var ftpRequestResponse = new FtpRequestResponse();
try
{
ArgumentNullException.ThrowIfNull(queueRequest);
var request = JsonConvert.DeserializeObject<SftpDownloadFileRequest>(queueRequest.Request);
ArgumentNullException.ThrowIfNull(request);
ftpRequestResponse.ReturnResponse = request;
ftpRequestResponse = TransferFiles(request, ftpRequestResponse);
}
catch (Exception exception)
{
ftpRequestResponse.IsSuccessful = false;
ftpRequestResponse.MessageStack.Add(exception.Message);
}
return ftpRequestResponse;
}
private FtpRequestResponse TransferFiles(SftpDownloadFileRequest winScpDownloadFileRequest, FtpRequestResponse ftpRequestResponse)
{
try
{
var listDirectoryRequest = new ListDirectoryRequest()
{
SessionInformation = winScpDownloadFileRequest.SessionInformation,
RemotePath = winScpDownloadFileRequest.FinalRemoteFilePath
};
var filesInDirectory = _winScpListDirectoryService.GetListOfFilesInDirectory(listDirectoryRequest);
var remoteFileNameRegex = winScpDownloadFileRequest.DynamicNameSetting != null ?
RegexNameHandler.BuildRegexName(winScpDownloadFileRequest.FinalRemoteFileName, winScpDownloadFileRequest.DynamicNameSetting) :
new Regex(winScpDownloadFileRequest.FinalRemoteFileName);
var matchedDirectoryFiles = filesInDirectory.Where(fileName => remoteFileNameRegex.IsMatch(fileName)).ToList();
if (!matchedDirectoryFiles.Any())
{
ftpRequestResponse.MessageStack.Add($"No files available to be downloaded. " +
$"RegexFileName: {remoteFileNameRegex}. " +
$"Succeed On Empty?: {winScpDownloadFileRequest.SucceedOnEmpty}");
ftpRequestResponse.IsSuccessful = winScpDownloadFileRequest.SucceedOnEmpty;
return ftpRequestResponse;
}
var sessionOptions = WinScpSessionOptionsHelper.CreateSessionOptions(winScpDownloadFileRequest.SessionInformation);
WinScpFile(sessionOptions, winScpDownloadFileRequest, matchedDirectoryFiles);
}
catch (Exception exception)
{
ftpRequestResponse.MessageStack.Add($"Failed to download file with WinSCP. " +
$"Host: {winScpDownloadFileRequest.SessionInformation.HostName}, " +
$"RemotePath: {winScpDownloadFileRequest.FinalRemoteFilePath} " +
$"ExceptionMessage: { exception.Message }");
ftpRequestResponse.IsSuccessful = false;
}
return ftpRequestResponse;
}
private void WinScpFile(SessionOptions sessionOptions, SftpDownloadFileRequest winScpDownloadFileRequest, List<string> remoteFileNames)
{
using (var session = new Session())
{
session.Open(sessionOptions);
var transferOptions = new TransferOptions { TransferMode = TransferMode.Binary, PreserveTimestamp = false };
if (!winScpDownloadFileRequest.ResumeSupportStateOn)
{
transferOptions.ResumeSupport.State = TransferResumeSupportState.Off;
}
foreach(string remoteFileName in remoteFileNames)
{
var addedSlash = winScpDownloadFileRequest.FinalRemoteFilePath.EndsWith('/') ? "" : "/";
var remotefilePath = $"{winScpDownloadFileRequest.FinalRemoteFilePath}{addedSlash}{remoteFileName}";
var transferResult = session.GetFiles(remotefilePath, winScpDownloadFileRequest.FinalLocalFilePath, winScpDownloadFileRequest.ShouldRemove, transferOptions);
transferResult.Check();
foreach (TransferEventArgs transfer in transferResult.Transfers)
{
Log.Debug($"Download of {transfer.FileName} succeeded");
}
}
}
}
}
@@ -0,0 +1,94 @@
using Newtonsoft.Json;
using Serilog;
using System.Linq;
using System.Collections.Generic;
using System;
using WinSCP;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Api.Internal.FTP.Helpers;
namespace LCU.Api.Internal.FTP.Services.FtpOpertionServices;
public class WinScpSftpUploadFileService : IWinScpSftpUploadFileService
{
IFileOperationsService _fileOperationsService;
public WinScpSftpUploadFileService(IFileOperationsService fileOperationsService)
{
_fileOperationsService = fileOperationsService;
}
public FtpRequestResponse ExecuteAction(QueueRequest queueRequest)
{
var ftpRequestResponse = new FtpRequestResponse();
try
{
ArgumentNullException.ThrowIfNull(queueRequest);
var request = JsonConvert.DeserializeObject<SftpUploadFileRequest>(queueRequest.Request);
ArgumentNullException.ThrowIfNull(request);
ftpRequestResponse.ReturnResponse = request;
ftpRequestResponse = TransferFiles(request, ftpRequestResponse);
}
catch (Exception exception)
{
ftpRequestResponse.IsSuccessful = false;
ftpRequestResponse.MessageStack.Add(exception.Message);
}
return ftpRequestResponse;
}
private FtpRequestResponse TransferFiles(SftpUploadFileRequest winScpUploadFileRequest, FtpRequestResponse ftpRequestResponse)
{
try
{
var filesInDirectory = _fileOperationsService.GetListOfFilesInDirectory(winScpUploadFileRequest).Result;
if (!filesInDirectory.Any())
{
ftpRequestResponse.MessageStack.Add($"No files available to be uploaded. " +
$"RegexFileName: {winScpUploadFileRequest.LocalFileName}. " +
$"Succeed On Empty?: {winScpUploadFileRequest.SucceedOnEmpty}");
ftpRequestResponse.IsSuccessful = winScpUploadFileRequest.SucceedOnEmpty;
return ftpRequestResponse;
}
var sessionOptions = WinScpSessionOptionsHelper.CreateSessionOptions(winScpUploadFileRequest.SessionInformation);
WinScpFile(sessionOptions, winScpUploadFileRequest.FinalLocalFilePath, winScpUploadFileRequest.FinalRemoteFilePath, filesInDirectory, winScpUploadFileRequest.ResumeSupportStateOn);
}
catch (Exception exception)
{
ftpRequestResponse.MessageStack.Add($"Failed to upload file with WinSCP. " +
$"Host: {winScpUploadFileRequest.SessionInformation.HostName}, " +
$"File: {winScpUploadFileRequest.FinalLocalFilePath}" +
$"ExceptionMessage: {exception.Message}");
ftpRequestResponse.IsSuccessful = false;
}
return ftpRequestResponse;
}
private void WinScpFile(SessionOptions sessionOptions, string filePath, string RemotePath, List<string> localFileNames, bool supportStateOn)
{
using (var session = new Session())
{
session.Open(sessionOptions);
var transferOptions = new TransferOptions { TransferMode = TransferMode.Binary, PreserveTimestamp = false };
if(!supportStateOn)
{
transferOptions.ResumeSupport.State = TransferResumeSupportState.Off;
}
var addedSlash = filePath.EndsWith(@"\") ? "" : @"\";
foreach (string fileName in localFileNames)
{
var localfilePath = $"{filePath}{addedSlash}{fileName}";
var transferResult = session.PutFiles(localfilePath, RemotePath, false, transferOptions);
transferResult.Check();
foreach (TransferEventArgs transfer in transferResult.Transfers)
{
Log.Debug($"Upload of {transfer.FileName} succeeded");
}
}
}
}
}
@@ -0,0 +1,95 @@
using System;
using Serilog;
using LCU.Library.Internal.ApiResponse.Services;
using LCU.Api.Internal.FTP.Configuration;
using LCU.Api.Internal.FTPLib.Models.Enums;
using LCU.Api.Internal.FTP.Services.FtpOpertionServices;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Api.Internal.FTP.Helpers;
using LCU.Library.Internal.Exceptions;
namespace LCU.Api.Internal.FTP.Services;
public class FtpService : IFtpService
{
private readonly IQueueFtpService _queueSftpService;
private readonly IWinScpSftpUploadFileService _winScpSftpUploadFilesService;
private readonly IWinScpSftpDownloadFilesService _winScpSftpDownloadFilesService;
private readonly ISshFtpService _sshFtpService;
private readonly FtpApiConfiguration _ftpApiConfiguration;
public FtpService(IQueueFtpService queueSftpService,
IWinScpSftpUploadFileService winScpSftpUploadFilesService,
IWinScpSftpDownloadFilesService winScpSftpDownloadFilesService,
ISshFtpService sshFtpService,
FtpApiConfiguration ftpApiConfiguration)
{
_queueSftpService = queueSftpService;
_winScpSftpUploadFilesService = winScpSftpUploadFilesService;
_winScpSftpDownloadFilesService = winScpSftpDownloadFilesService;
_sshFtpService = sshFtpService;
_ftpApiConfiguration = ftpApiConfiguration;
}
public void HandleFtpRequest(QueueRequest queueRequest)
{
var result = PerformFtpOperation(queueRequest);
if (ShouldPerformFtpRequestAgain(queueRequest, result))
{
Log.Information($"Attempting to requeue ftp request. FtpType: {queueRequest.FtpType}");
queueRequest.Attempts++;
_queueSftpService.QueueFtpRequest(queueRequest);
return;
}
QueueReturnResponse.QueueResponse(result.ReturnResponse, result.IsSuccessful, result.MessageStack);
if (!result.IsSuccessful)
{
var errorMessage = String.Join("; ", result.MessageStack.ToArray());
Log.Error($"Ftp Request was not successful. MessageStack:{errorMessage}");
throw new ServiceException($"Ftp Request was not successful. MessageStack:{errorMessage}");
}
}
public FtpRequestResponse PerformFtpOperation(QueueRequest queueRequest)
{
var ftpRequestResponse = new FtpRequestResponse();
try
{
ArgumentNullException.ThrowIfNull(queueRequest);
ftpRequestResponse = queueRequest.FtpType switch
{
FtpType.WinScpSftpUpload => _winScpSftpUploadFilesService.ExecuteAction(queueRequest),
FtpType.WinScpSftpDownload => _winScpSftpDownloadFilesService.ExecuteAction(queueRequest),
FtpType.SshFtp => _sshFtpService.ExecuteAction(queueRequest),
_ => throw new NotImplementedException()
};
}
catch (Exception exception)
{
var failureMessage = $"Failed to perform ftp request.";
Log.Error(exception, failureMessage);
var fileOperationType = queueRequest?.FtpType;
if (fileOperationType == null)
{
ftpRequestResponse.MessageStack.Add($"{failureMessage}. FtpType is Null. " +
$"ExceptionMessage: {exception.Message}");
return ftpRequestResponse;
}
ftpRequestResponse = FtpRequestResponseHelper.ExceptionUpdateRequestResponse(ftpRequestResponse, queueRequest.FtpType, exception.Message);
}
return ftpRequestResponse;
}
public bool ShouldPerformFtpRequestAgain(QueueRequest request, FtpRequestResponse ftpRequestResponse)
{
return !ftpRequestResponse.IsSuccessful &&
request.Attempts != null &&
request.Attempts < _ftpApiConfiguration.AttemptsLimit;
}
}
@@ -0,0 +1,8 @@
using System.Threading;
using LCU.Api.Internal.FTPLib.Models;
namespace LCU.Api.Internal.FTP.Services;
public interface IFtpService
{
void HandleFtpRequest(QueueRequest queueRequest);
}
@@ -0,0 +1,14 @@
using System;
using LCU.Api.Internal.FTPLib.Models;
namespace LCU.Api.Internal.FTP.Services;
public interface IQueueFtpService
{
[Obsolete("01/04/2022 - After the SftpController endpoints are no longer in use this can be removed.")]
void QueueSftp(SftpRequest request);
[Obsolete("01/04/2022 - After the SftpController endpoints are no longer in use this can be removed.")]
void QueueSftpDynamicFileRequest(SftpDynamicFileRequest sftpDynamicFileRequest);
void QueueSftpUploadRequest(SftpUploadFileRequest winScpUploadFileRequest);
void QueueSftpDownloadRequest(SftpDownloadFileRequest winScpDownloadFileRequest);
void QueueFtpRequest(QueueRequest queueRequest);
}
@@ -0,0 +1,7 @@
using LCU.Api.Internal.FTPLib.Models;
namespace LCU.Api.Internal.FTP.Services;
public interface IQueueSshFtpService
{
void QueueSshFtpRequest(SshFtpRequest request);
}
@@ -0,0 +1,181 @@
using System;
using Newtonsoft.Json;
using Serilog;
using MassTransit;
using LCU.Api.Internal.FTP.Configuration;
using LCU.Api.Internal.FTPLib.Models.Enums;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Library.Internal.Exceptions;
using LCU.Library.Internal.LcuMassTransitModels;
namespace LCU.Api.Internal.FTP.Services;
public class QueueFtpService : IQueueFtpService
{
private readonly IQueueSshFtpService _queueSshFtpService;
private readonly ISendEndpoint _sendEndpoint;
public QueueFtpService(IQueueSshFtpService queueSshFtpService,
IBusControl busControl,
ReceiveEndpointConfiguration receiveEndpointsConfiguration)
{
_queueSshFtpService = queueSshFtpService;
_sendEndpoint = busControl.GetSendEndpoint(receiveEndpointsConfiguration.ReceiveEndpointUri).Result;
}
[Obsolete("01/04/2022 - After the SftpController endpoints that call this method are no longer in use this can be removed.")]
public void QueueSftp(SftpRequest request)
{
try
{
//If it has localfiledata then we need to use sshftp to accommodate old endpoints.
if (request.LocalFileData != null && request.LocalFileData.Length > 0)
{
var sshFtpRequest = new SshFtpRequest()
{
Protocol = request.Protocol,
HostName = request.HostName,
PortNumber = request.PortNumber,
UserName = request.UserName,
Password = request.Password,
RemoteFilePath = request.RemoteFilePath,
LocalFileData = request.LocalFileData
};
_queueSshFtpService.QueueSshFtpRequest(sshFtpRequest);
return;
}
var winScpUploadFileRequest = new SftpUploadFileRequest()
{
SessionInformation = new SessionInformation()
{
Protocol = request.Protocol,
HostName = request.HostName,
PortNumber = request.PortNumber,
UserName = request.UserName,
Password = request.Password,
SshHostKeyFingerprint = request.SshHostKeyFingerprint
},
LocalFilePath = request.LocalFilePath,
LocalFileName = null,
RemoteFilePath = request.RemoteFilePath,
DynamicNameSetting = null,
ResponseItems = request.ResponseItems
};
var queueRequest = new QueueRequest()
{
Request = JsonConvert.SerializeObject(winScpUploadFileRequest),
FtpType = FtpType.WinScpSftpUpload
};
_sendEndpoint.Send(queueRequest);
Log.Debug($"Queued The Following Sftp Request {request}");
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to Queue Sftp Request. Request: {request}");
throw new ServiceException($"Failed to Queue Sftp Request. Request: {request}");
}
}
[Obsolete("01/04/2022 - After the SftpController endpoints that call this method are no longer in use this can be removed.")]
public void QueueSftpDynamicFileRequest(SftpDynamicFileRequest request)
{
try
{
var winScpUploadFileRequest = new SftpUploadFileRequest()
{
SessionInformation = new SessionInformation()
{
Protocol = request.Protocol,
HostName = request.HostName,
PortNumber = request.PortNumber,
UserName = request.UserName,
Password = request.Password,
SshHostKeyFingerprint = request.SshHostKeyFingerprint
},
LocalFilePath = request.LocalFilePath,
LocalFileName = request.LocalFileName,
RemoteFilePath = request.RemoteFilePath,
DynamicNameSetting = request.DynamicNameSetting,
ResponseItems = request.ResponseItems
};
var queueRequest = new QueueRequest()
{
Request = JsonConvert.SerializeObject(winScpUploadFileRequest),
FtpType = FtpType.WinScpSftpUpload
};
_sendEndpoint.Send(queueRequest);
Log.Debug($"Queued The Following Sftp Request to host {request.HostName}");
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to Queue Sftp dynamic file request. " +
$"Host: {request.HostName}");
throw new ServiceException($"Failed to Queue Sftp dynamic file request. " +
$"Host: {request.HostName}");
}
}
public void QueueSftpUploadRequest(SftpUploadFileRequest request)
{
try
{
var queueRequest = new QueueRequest()
{
Request = JsonConvert.SerializeObject(request),
FtpType = FtpType.WinScpSftpUpload
};
_sendEndpoint.Send(queueRequest);
Log.Debug($"Queued sftp upload request to " +
$"host {request.SessionInformation.HostName}");
}
catch (Exception exception)
{
Log.Error(exception, $"Failed to Queue Sftp Upload Request. " +
$"Host: {request.SessionInformation.HostName}");
throw new ServiceException($"Failed to Queue Sftp Upload Request. " +
$"Host: {request.SessionInformation.HostName}");
}
}
public void QueueSftpDownloadRequest(SftpDownloadFileRequest request)
{
try
{
var queueRequest = new QueueRequest()
{
Request = JsonConvert.SerializeObject(request),
FtpType = FtpType.WinScpSftpDownload
};
_sendEndpoint.Send(queueRequest);
Log.Debug($"Queued sftp download request from " +
$"host {request.SessionInformation.HostName}");
}
catch (Exception exception)
{
Log.Error(exception, $"Failed to Queue Sftp Download Request. " +
$"Host: {request.SessionInformation.HostName}");
throw new ServiceException($"Failed to Queue Sftp Download Request. " +
$"Host: {request.SessionInformation.HostName}");
}
}
public void QueueFtpRequest(QueueRequest queueRequest)
{
try
{
_sendEndpoint.Send(queueRequest);
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to Queue ftp request. FtpType: {queueRequest.FtpType}");
throw new ServiceException($"Failed to Queue ftp request. FtpType: {queueRequest.FtpType}");
}
}
}
@@ -0,0 +1,42 @@
using System;
using Newtonsoft.Json;
using Serilog;
using MassTransit;
using LCU.Api.Internal.FTP.Configuration;
using LCU.Api.Internal.FTPLib.Models.Enums;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Library.Internal.Exceptions;
using LCU.Library.Internal.LcuMassTransitModels;
namespace LCU.Api.Internal.FTP.Services;
public class QueueSshFtpService : IQueueSshFtpService
{
private readonly ISendEndpoint _sendEndpoint;
public QueueSshFtpService(IBusControl busControl, ReceiveEndpointConfiguration receiveEndpointsConfiguration)
{
_sendEndpoint = busControl.GetSendEndpoint(receiveEndpointsConfiguration.ReceiveEndpointUri).Result;
}
public void QueueSshFtpRequest(SshFtpRequest request)
{
try
{
var queueRequest = new QueueRequest();
queueRequest = new QueueRequest()
{
Request = JsonConvert.SerializeObject(request),
FtpType = FtpType.SshFtp
};
_sendEndpoint.Send(queueRequest);
Log.Debug($"Queued ssh ftp request to host {request.HostName}");
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to queue ssh ftp request to host {request.HostName}");
throw new ServiceException($"Failed to queue ssh ftp request to host {request.HostName}");
}
}
}
@@ -0,0 +1,159 @@
//System
using System;
using System.IO;
using System.Reflection;
//Microsoft
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
//Others
using MassTransit;
//FTP
using LCU.Api.Internal.FTP.Configuration;
using LCU.Api.Internal.FTP.Security;
using LCU.Api.Internal.FTP.Services.FtpOpertionServices;
using LCU.Api.Internal.FTP.Services;
using LCU.Api.Internal.FTP.Consumers;
using LCU.Library.Internal.LcuMassTransitModels;
using LCU.Library.Internal.LcuMassTransit;
namespace LCU.Api.Internal.FTP;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddCors(options =>
{
options.AddPolicy(MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://localhost:3000", "https://localhost:5001",
"http://lcu-internal.com", "https://lcu-internal.com").AllowAnyHeader()
.AllowAnyMethod();
});
});
//Configurations
FtpApiConfiguration ftpApiConfiguration = new FtpApiConfiguration();
Configuration.GetSection("Primary").Bind(ftpApiConfiguration);
services.AddSingleton(ftpApiConfiguration);
services.AddHttpClient();
var connectionConfiguration = MassTransitConfigurationSetUp.AddConnectionConfiguration(ref services, Configuration);
var receiveEndpointConfiguration = MassTransitConfigurationSetUp.GetReceiveEndpointConfiguration(Configuration,
"FtpReceiveEndpoint");
services.AddSingleton(receiveEndpointConfiguration);
AddMassTransitServices(ref services, connectionConfiguration, receiveEndpointConfiguration);
//Services
services.AddScoped<IEnvironmentView, EnvironmentView>();
services.AddScoped<IQueueFtpService, QueueFtpService>();
services.AddScoped<IQueueSshFtpService, QueueSshFtpService>();
services.AddScoped<IFtpService, FtpService>();
services.AddScoped<IWinScpSftpUploadFileService, WinScpSftpUploadFileService>();
services.AddScoped<IWinScpSftpDownloadFilesService, WinScpSftpDownloadFilesService>();
services.AddScoped<IWinScpListDirectoryService, WinScpListDirectoryService>();
services.AddScoped<ISshFtpService, SshFtpService>();
services.AddScoped<IFileOperationsService, FileOperationsService>();
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Ftp API", Version = "v1" });
// For this to work, I also had to check the generate xml box in build options.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.XML";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
//Set up Authentication
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
// AzureAD config
.AddAzureAdBearer(options => Configuration.GetSection("AzureActiveDirectory").Bind(options));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultTokenProviders();
}
public void AddMassTransitServices(ref IServiceCollection services,
ConnectionConfiguration connectionConfiguration,
ReceiveEndpointConfiguration receiveEndpointConfiguration)
{
services.AddMassTransit(x =>
{
x.AddConsumer<SftpConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(connectionConfiguration.HostUrl, "/", host =>
{
host.Username(connectionConfiguration.UserName);
host.Password(connectionConfiguration.Password);
});
cfg.ReceiveEndpoint(receiveEndpointConfiguration.ReceiveEndpoint, e =>
{
e.ConfigureConsumer<SftpConsumer>(context);
e.SetQuorumQueue();
});
});
});
services.AddMassTransitHostedService();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Ftp API");
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseAuthentication();
app.UseCors(MyAllowSpecificOrigins);
app.UseHttpsRedirection();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
}
);
}
}
@@ -0,0 +1,47 @@
{
"Serilog": {
"LevelSwitches": { "$controlSwitch": "Information" },
"MinimumLevel": {
"ControlledBy": "$controlSwitch",
"Override": {
"Microsoft": "Information",
"System": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"WriteTo": [
{
"Name": "Seq",
"Args": {
"serverUrl": "https://qa-seq.lcu-internal.com",
"apiKey": "vG5LVLks9L5ae7oWAISN",
"controlLevelSwitch": "$controlSwitch"
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
"Properties": {
"Application": "FTP Api",
"Environment": "Debug"
}
},
"AllowedHosts": "*",
"Primary": {
"AttemptsLimit": 1,
"Apis": {
"FileOperationApi": {
"BuildDynamicFileRequest": "https://localhost:44310/DynamicFile/BuildDynamicFileList",
"ValidateFileExists": "https://localhost:44310/FileValidation/ValidateFileExists"
}
}
},
"ConnectionConfiguration": {
"HostUrl": "localhost",
"UserName": "guest",
"Password": "guest"
},
"FtpReceiveEndpoint": {
"ReceiveEndpoint": "LCU.FTP",
"ReceiveEndpointType": "exchange"
}
}
@@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>2.11.2</Version>
<PackageVersion>2.11.3</PackageVersion>
<Description>
1.0.0 - Initial Creation
1.1.0 - Added ProtocolType and removed WinSCP
2.0.0 - Adding PortNumber that is required but defaulted to 22
2.1.0 - Add SftpResponse
2.2.0 - Update Queue and ApiResponse Libraries
2.3.0 - Add SftpDynamicFileRequest
2.4.0 - Added SftpUploadfileRequest, SftpDownLoadFileRequest, QueueRequest(from api models), SshFtpRequest, and FtpRequestResponse
2.5.0 - Upgrade ApiResponse and FileOperations Libraries
2.6.0 - New ApiResponse nuget to use ExchangeName.
2.7.0 - Update to SftpUploadFileRequest
2.8.0 - Update to SftpDownloadFileRequest. Adds should remove true or false toggle
2.9.0 - Update upload and download. Add SucceeddOnEmpty toggle. Alteratiosn to FileOperationsService.
2.9.1 - Added DynamicNameSetting for local and remote file paths
2.10.0 - Fix Download/Uploads local/remote paths to append slashes on final.
2.11.0 - Add ResumeSupport toggle.
2.11.1 - Add SshKeyFilePath to SessionInformation
2.11.2 - Add ResumeSupport toggle to download request
2.11.3 - Upgraded FileOperations package version
</Description>
<PackageReleaseNotes>
1.0.0 - Initial Creation
1.1.0 - Added ProtocolType and removed WinSCP
2.0.0 - Adding PortNumber that is required but defaulted to 22
2.1.0 - Add SftpResponse
2.2.0 - Update Queue and ApiResponse Libraries
2.3.0 - Add SftpDynamicFileRequest
2.4.0 - Added SftpUploadfileRequest, SftpDownLoadFileRequest, QueueRequest(from api models), SshFtpRequest, and FtpRequestResponse
2.5.0 - Upgrade ApiResponse and FileOperations Libraries
2.6.0 - New ApiResponse nuget to use ExchangeName.
2.7.0 - Update to SftpUploadFileRequest
2.8.0 - Update to SftpDownloadFileRequest. Adds should remove true or false toggle
2.9.0 - Update upload and download. Add SucceeddOnEmpty toggle. Alteratiosn to FileOperationsService.
2.9.1 - Added DynamicNameSetting for local and remote file paths
2.10.0 - Fix Download/Uploads local/remote paths to append slashes on final.
2.11.0 - Add ResumeSupport toggle.
2.11.1 - Add SshKeyFilePath to SessionInformation
2.11.2 - Add ResumeSupport toggle to download request
2.11.3 - Upgraded FileOperations package version
</PackageReleaseNotes>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Title>LCU Internal FTP Library</Title>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LCU.Library.Internal.ApiResponse.Models" Version="3.3.0" />
<PackageReference Include="LCU.Library.Internal.FileOperationsModels" Version="4.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.DataAnnotations" Version="2.2.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,10 @@
namespace LCU.Api.Internal.FTPLib.Models.Enums
{
public enum FtpType
{
Other,
WinScpSftpDownload,
WinScpSftpUpload,
SshFtp
}
}
@@ -0,0 +1,21 @@
using LCU.Library.Internal.ApiResponse.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace LCU.Api.Internal.FTPLib.Models
{
public class FtpRequestResponse
{
public FtpRequestResponse()
{
MessageStack = new List<string>();
IsSuccessful = true;
ReturnResponse = null;
}
public List<string> MessageStack { get; set; }
public bool IsSuccessful { get; set; }
public ReturnResponse ReturnResponse { get; set; }
}
}
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace LCU.Api.Internal.FTPLib.Models
{
public class ListDirectoryRequest
{
public SessionInformation SessionInformation { get; set; }
[Required]
public string RemotePath { get; set; }
}
}
@@ -0,0 +1,7 @@
namespace LCU.Api.Internal.FTPLib.Models
{
public enum ProtocolType
{
Sftp
}
}
@@ -0,0 +1,18 @@
using LCU.Api.Internal.FTPLib.Models.Enums;
namespace LCU.Api.Internal.FTPLib.Models
{
public class QueueRequest
{
public QueueRequest()
{
Guid = System.Guid.NewGuid().ToString();
Attempts = 0;
}
public string Guid { get; }
public string Request { get; set; }
public int? Attempts { get; set; }
public FtpType FtpType { get; set; }
}
}
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace LCU.Api.Internal.FTPLib.Models
{
public class SessionInformation
{
[Required]
public ProtocolType Protocol { get; set; }
[Required]
public string HostName { get; set; }
[Required, Range(1, 999999)]
public int PortNumber { get; set; } = 22;
[Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
[Required]
public string SshHostKeyFingerprint { get; set; }
public string SshKeyFilePath { get; set; }
}
}
@@ -0,0 +1,60 @@
using LCU.Library.Internal.ApiResponse.Models;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using LCU.Library.Internal.FileOperationsModels.Models.NameSettings;
using LCU.Library.Internal.FileOperationsModels.Shared;
using System.ComponentModel;
namespace LCU.Api.Internal.FTPLib.Models
{
public class SftpDownloadFileRequest : ReturnResponse
{
[Required]
public SessionInformation SessionInformation { get; set; }
[Required]
public string RemoteFilePath { get; set; }
[Required]
public string RemoteFileName { get; set; }
[Required]
public string LocalFilePath { get; set; }
[DefaultValue(false)]
public bool ShouldRemove { get; set; }
[DefaultValue(false)]
public bool SucceedOnEmpty { get; set; }
public DynamicNameSetting DynamicNameSetting { get; set; }
[DefaultValue(true)]
public bool ResumeSupportStateOn { get; set; } = true;
[NotMapped]
public string FinalRemoteFileName
{
get
{
return DynamicNameBuilder.BuildDynamicName(RemoteFileName, DynamicNameSetting);
}
}
public DynamicNameSetting DynamicRemotePathSetting { get; set; }
public DynamicNameSetting DynamicLocalPathSetting { get; set; }
[NotMapped]
public string FinalRemoteFilePath
{
get
{
var remoteFilePath = RemoteFilePath.EndsWith("/") ? RemoteFilePath : $"{RemoteFilePath}/";
return DynamicNameBuilder.BuildDynamicName(remoteFilePath, DynamicRemotePathSetting);
}
}
[NotMapped]
public string FinalLocalFilePath
{
get
{
var localFilePath = LocalFilePath.EndsWith("\\") ? LocalFilePath : $"{LocalFilePath}\\";
return DynamicNameBuilder.BuildDynamicName(localFilePath, DynamicLocalPathSetting);
}
}
}
}
@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations.Schema;
using LCU.Library.Internal.FileOperationsModels.Models.NameSettings;
using LCU.Library.Internal.FileOperationsModels.Shared;
namespace LCU.Api.Internal.FTPLib.Models
{
public class SftpDynamicFileRequest : SftpRequest
{
[NotMapped]
public string FinalLocalFilePath
{
get
{
return $"{LocalFilePath}\\{DynamicNameBuilder.BuildDynamicName(LocalFileName, DynamicNameSetting)}";
}
}
public string LocalFileName { get; set; }
public DynamicNameSetting DynamicNameSetting { get; set; }
}
}
@@ -0,0 +1,38 @@
using System.Collections.Generic;
using LCU.Library.Internal.ApiResponse.Models;
using System.ComponentModel.DataAnnotations;
using System;
namespace LCU.Api.Internal.FTPLib.Models
{
[Obsolete("Please use SftpDownloadFileRequest and SshFtpRequst Models")]
public class SftpRequest : ReturnResponse, IValidatableObject
{
[Required]
public ProtocolType Protocol { get; set; }
[Required]
public string HostName { get; set; }
[Required, Range(1, 999999)]
public int PortNumber { get; set; } = 22;
[Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
[Required]
public string SshHostKeyFingerprint { get; set; }
public string LocalFilePath { get; set; }
[Required]
public string RemoteFilePath { get; set; }
public byte[] LocalFileData { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(LocalFilePath) && LocalFileData == null)
{
yield return new ValidationResult(
$"One of LocalFilePath or LocalFileData bytes are required.",
new[] {nameof(LocalFileData), nameof(LocalFilePath)});
}
}
}
}
@@ -0,0 +1,11 @@
namespace LCU.Api.Internal.FTPLib.Models
{
// This should really throw on BadRequest and Error but I'm sticking to the original code intention for now.
public enum SftpResponse
{
BadRequest,
Success,
FailNoRetry,
FailWillRetry
}
}
@@ -0,0 +1,62 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using LCU.Library.Internal.ApiResponse.Models;
using LCU.Library.Internal.FileOperationsModels.Models.NameSettings;
using LCU.Library.Internal.FileOperationsModels.Shared;
using System.ComponentModel;
namespace LCU.Api.Internal.FTPLib.Models
{
public class SftpUploadFileRequest : ReturnResponse
{
[Required]
public SessionInformation SessionInformation { get; set; }
[Required]
public string LocalFilePath { get; set; }
[Required]
public string LocalFileName { get; set; }
[Required]
public string RemoteFilePath { get; set; }
public DynamicNameSetting DynamicNameSetting { get; set; }
[DefaultValue(false)]
public bool SucceedOnEmpty { get; set; } = false;
[DefaultValue(true)]
public bool ResumeSupportStateOn { get; set; } = true;
[NotMapped]
public string FinalLocalFileName
{
get
{
if (string.IsNullOrWhiteSpace(LocalFileName))
{
return LocalFileName;
}
return DynamicNameBuilder.BuildDynamicName(LocalFileName, DynamicNameSetting);
}
}
public DynamicNameSetting DynamicRemotePathSetting { get; set; }
public DynamicNameSetting DynamicLocalPathSetting { get; set; }
[NotMapped]
public string FinalRemoteFilePath
{
get
{
var remoteFilePath = RemoteFilePath.EndsWith("/") ? RemoteFilePath : $"{RemoteFilePath}/";
return DynamicNameBuilder.BuildDynamicName(remoteFilePath, DynamicRemotePathSetting);
}
}
[NotMapped]
public string FinalLocalFilePath
{
get
{
var localFilePath = LocalFilePath.EndsWith("\\") ? LocalFilePath : $"{LocalFilePath}\\";
return DynamicNameBuilder.BuildDynamicName(localFilePath, DynamicLocalPathSetting);
}
}
}
}
@@ -0,0 +1,22 @@
using LCU.Library.Internal.ApiResponse.Models;
using System.ComponentModel.DataAnnotations;
namespace LCU.Api.Internal.FTPLib.Models
{
public class SshFtpRequest : ReturnResponse
{
[Required]
public ProtocolType Protocol { get; set; }
[Required]
public string HostName { get; set; }
[Required, Range(1, 999999)]
public int PortNumber { get; set; } = 22;
[Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
[Required]
public string RemoteFilePath { get; set; }
public byte[] LocalFileData { get; set; }
}
}
@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Mvc;
using Moq;
using Xunit;
using LCU.Api.Internal.FTP.Controllers;
using LCU.Api.Internal.FTP.Services;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Api.Internal.FTP.Services.FtpOpertionServices;
namespace LCU.Api.Internal.FTPTests.Controllers;
public class SftpControllerTest
{
private readonly SftpController controller;
private readonly Mock<IQueueFtpService> _mockFtpService;
private readonly Mock<IWinScpListDirectoryService> _winScpListDirectoryService;
public SftpControllerTest()
{
_mockFtpService = new Mock<IQueueFtpService>();
_winScpListDirectoryService = new Mock<IWinScpListDirectoryService>();
controller = new SftpController(_mockFtpService.Object, _winScpListDirectoryService.Object);
}
// Post tests
[Fact]
public void Post_OK()
{
// arrange
var request = new SftpRequest()
{
Protocol = ProtocolType.Sftp,
HostName = "Test Host",
UserName = "Test UserName",
Password = "Test Password",
LocalFilePath = "Test LocalFilePath",
RemoteFilePath = "Test RemoteFilePath"
};
Assert.Equal(22, request.PortNumber);
// act
var result = controller.Post(request);
Assert.NotNull(result);
var okResult = result as OkResult;
// assert
Assert.NotNull(okResult);
Assert.Equal(200, okResult.StatusCode);
}
// Post tests
[Fact]
public void Post_InvalidSftp_NoHost()
{
// arrange
var request = new SftpRequest() { };
// act
var result = controller.Post(request);
Assert.NotNull(result);
var badRequest = result as BadRequestObjectResult;
Assert.Equal(400, badRequest.StatusCode);
Assert.Equal("Sftp request can not be created into a valid session options.", badRequest.Value);
}
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="Moq" Version="4.13.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LCU.Api.Internal.FTP\LCU.Api.Internal.FTP.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,236 @@
using Moq;
using Xunit;
using Newtonsoft.Json;
using LCU.Api.Internal.FTP.Configuration;
using LCU.Api.Internal.FTP.Services;
using LCU.Api.Internal.FTP.Services.FtpOpertionServices;
using LCU.Api.Internal.FTPLib.Models;
using LCU.Api.Internal.FTPLib.Models.Enums;
namespace LCU.Api.Internal.FTPTests.Services;
public class FtpServiceTest
{
private readonly FtpService _ftpService;
private Mock<IQueueFtpService> _mockQueueFtpService;
private Mock<IWinScpSftpUploadFileService> _mockWinScpUploadFileService;
private Mock<IWinScpSftpDownloadFilesService> _mockWinScpDownloadFilesService;
private Mock<ISshFtpService> _mockSshFtpService;
public FtpServiceTest()
{
var ftpApiConfiguration = new FtpApiConfiguration()
{
AttemptsLimit = 2
};
_mockQueueFtpService = new Mock<IQueueFtpService>();
_mockWinScpUploadFileService = new Mock<IWinScpSftpUploadFileService>();
_mockWinScpDownloadFilesService = new Mock<IWinScpSftpDownloadFilesService>();
_mockSshFtpService = new Mock<ISshFtpService>();
_ftpService = new FtpService(_mockQueueFtpService.Object,
_mockWinScpUploadFileService.Object,
_mockWinScpDownloadFilesService.Object,
_mockSshFtpService.Object,
ftpApiConfiguration);
}
[Fact]
public void Basic_Construct_Test()
{
Assert.NotNull(_ftpService);
}
[Fact]
public void PerformFtpOperation_WinScpUploadFileService_Ran_Returned_SuccessResponse()
{
var ftpRequestRequestResponse = new FtpRequestResponse()
{
IsSuccessful = true
};
_mockWinScpUploadFileService.Setup(x => x.ExecuteAction(It.IsAny<QueueRequest>())).Returns(ftpRequestRequestResponse);
var request = new SftpUploadFileRequest()
{
SessionInformation = new SessionInformation()
{
Protocol = 0,
HostName = "TestHostName",
PortNumber = 22,
UserName = "TestUserName",
Password = "TestPassword",
SshHostKeyFingerprint = "TestSshHostKeyFingerprint"
},
LocalFileName = "TestLocalFileName",
LocalFilePath = "\\TestLocalFilePath\\",
RemoteFilePath = "\\TestRemoteFilePath\\"
};
var queueRequest = new QueueRequest()
{
Request = JsonConvert.SerializeObject(request),
FtpType = FtpType.WinScpSftpUpload
};
var result = _ftpService.PerformFtpOperation(queueRequest);
Assert.True(result.IsSuccessful);
_mockWinScpUploadFileService.Verify(x => x.ExecuteAction(It.IsAny<QueueRequest>()), Times.Once);
_mockWinScpDownloadFilesService.Verify(x => x.ExecuteAction(It.IsAny<QueueRequest>()), Times.Never);
_mockSshFtpService.Verify(x => x.ExecuteAction(It.IsAny<QueueRequest>()), Times.Never);
}
[Fact]
public void PerformFtpOperation_WinScpDownloadFilesService_Ran_Returned_SuccessResponse()
{
var ftpRequestRequestResponse = new FtpRequestResponse()
{
IsSuccessful = true
};
_mockWinScpDownloadFilesService.Setup(x => x.ExecuteAction(It.IsAny<QueueRequest>())).Returns(ftpRequestRequestResponse);
var request = new SftpDownloadFileRequest()
{
SessionInformation = new SessionInformation()
{
Protocol = 0,
HostName = "TestHostName",
PortNumber = 22,
UserName = "TestUserName",
Password = "TestPassword",
SshHostKeyFingerprint = "TestSshHostKeyFingerprint"
},
LocalFilePath = "\\TestLocalFilePath\\",
RemoteFilePath = "\\TestRemoteFilePath\\",
RemoteFileName = "TestRemoteFileName"
};
var queueRequest = new QueueRequest()
{
Request = JsonConvert.SerializeObject(request),
FtpType = FtpType.WinScpSftpDownload
};
var result = _ftpService.PerformFtpOperation(queueRequest);
Assert.True(result.IsSuccessful);
_mockWinScpUploadFileService.Verify(x => x.ExecuteAction(It.IsAny<QueueRequest>()), Times.Never);
_mockWinScpDownloadFilesService.Verify(x => x.ExecuteAction(It.IsAny<QueueRequest>()), Times.Once);
_mockSshFtpService.Verify(x => x.ExecuteAction(It.IsAny<QueueRequest>()), Times.Never);
}
[Fact]
public void PerformFtpOperation_SshFtpService_Ran_Returned_SuccessResponse()
{
var ftpRequestRequestResponse = new FtpRequestResponse()
{
IsSuccessful = true
};
_mockSshFtpService.Setup(x => x.ExecuteAction(It.IsAny<QueueRequest>())).Returns(ftpRequestRequestResponse);
var request = new SshFtpRequest()
{
Protocol = 0,
HostName = "TestHostName",
PortNumber = 22,
UserName = "TestUserName",
Password = "TestPassword",
LocalFileData = new byte[1],
RemoteFilePath = "\\TestRemoteFilePath\\"
};
var queueRequest = new QueueRequest()
{
Request = JsonConvert.SerializeObject(request),
FtpType = FtpType.SshFtp
};
var result = _ftpService.PerformFtpOperation(queueRequest);
Assert.True(result.IsSuccessful);
_mockWinScpUploadFileService.Verify(x => x.ExecuteAction(It.IsAny<QueueRequest>()), Times.Never);
_mockWinScpDownloadFilesService.Verify(x => x.ExecuteAction(It.IsAny<QueueRequest>()), Times.Never);
_mockSshFtpService.Verify(x => x.ExecuteAction(It.IsAny<QueueRequest>()), Times.Once);
}
[Fact]
public void PerformFtpOperation_Exception_NoType_Returned_FailResponse()
{
var request = new SshFtpRequest()
{
Protocol = 0,
HostName = "TestHostName",
PortNumber = 22,
UserName = "TestUserName",
Password = "TestPassword",
LocalFileData = new byte[1],
RemoteFilePath = "\\TestRemoteFilePath\\"
};
var queueRequest = new QueueRequest()
{
Request = JsonConvert.SerializeObject(request)
};
var result = _ftpService.PerformFtpOperation(queueRequest);
Assert.False(result.IsSuccessful);
_mockWinScpUploadFileService.Verify(x => x.ExecuteAction(It.IsAny<QueueRequest>()), Times.Never);
_mockWinScpDownloadFilesService.Verify(x => x.ExecuteAction(It.IsAny<QueueRequest>()), Times.Never);
_mockSshFtpService.Verify(x => x.ExecuteAction(It.IsAny<QueueRequest>()), Times.Never);
}
[Fact]
public void ShouldPerformFtpRequestAgain_AttempsLessThenLimit_True()
{
var request = new QueueRequest()
{
Attempts = 0
};
var ftpRequestRequestResponse = new FtpRequestResponse()
{
IsSuccessful = false
};
var result = _ftpService.ShouldPerformFtpRequestAgain(request, ftpRequestRequestResponse);
Assert.True(result);
}
[Fact]
public void ShouldPerformFtpRequestAgain_AttempsGreaterThenLimit_False()
{
var request = new QueueRequest()
{
Attempts = 3
};
var ftpRequestRequestResponse = new FtpRequestResponse()
{
IsSuccessful = false
};
var result = _ftpService.ShouldPerformFtpRequestAgain(request, ftpRequestRequestResponse);
Assert.False(result);
}
[Fact]
public void ShouldPerformFtpRequestAgain_SuccessfullyFtpRequest_False()
{
var request = new QueueRequest()
{
Attempts = 1
};
var ftpRequestRequestResponse = new FtpRequestResponse()
{
IsSuccessful = true
};
var result = _ftpService.ShouldPerformFtpRequestAgain(request, ftpRequestRequestResponse);
Assert.False(result);
}
}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- additional settings used during the dotnet test execution step in the azure dev ops pipeline -->
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="Code Coverage" uri="datacollector://Microsoft/CodeCoverage/2.0" assemblyQualifiedName="Microsoft.VisualStudio.Coverage.DynamicCoverageDataCollector, Microsoft.VisualStudio.TraceCollector, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<Configuration>
<CodeCoverage>
<CompanyNames>
<Exclude>
<CompanyName>.*microsoft.*</CompanyName>
</Exclude>
</CompanyNames>
</CodeCoverage>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>
+78
View File
@@ -0,0 +1,78 @@
# Introduction
FTP API is used for making HTTP requests to transfer files over SFTP. Consumers can submit byte[] directly, or a shared directory path.
# Getting Started
The required environment is .NET Core 3.1, Git, and something to work with C#.
## Working With The Code
Before submitting your pull request, review the following:
* Did you commit with a commitizen style message?
* Did you add all needed unit testing, and ensure all existing tests are passing?
* Make sure you didn't accidentally commit any secrets or passwords.
# Contribute
Please propose contributions to shared libraries and APIs; especially breaking changes.
# Usage
The easiest way to see usage is to look at the Integration test project which has sample code, or hit the QA swagger.
TODO -> someone explain the shared file paths?
# Testing the API
You can use the following method to test the API locally or in any of the live environments. This method of testing will only test the SSH strategy. To use the WINSCP strategy would require a known file on the QA server.
1. Start a SFTP Docker Server with the following command on your local machine:
`docker run -p 22:22 -d emberstack/sftp --name sftp`
- This will create a simple SFTP server with the username: **demo** and password: **demo**
- The working directory will be /sftp
- You can access this through your computers full name which can be found in your PCs properties
- It should follow this format: FirstnameLastname.lcu-internal.com
2. Access the logs for this container either using Docker Desktop or `docker logs --follow <container ID>`
3. Run this command on the container `ssh-keygen -lf /etc/ssh/ssh_host_rsa_key.pub` to generate a ssh fingerprint.
* You can run this in the Docker Desktop by accessing the container cli
* Or run it using `docker exec -it <container ID> <Command>`
4. The command will generate a fingerprint that looks something like this:
- 4096 SHA256:7bxdtxb2niicuigfdrobojrsashnlwid6/jA9puDY80 root@984e6ac2415f (RSA)
- Reformat yours to match this pattern:
- `ssh-rsa 4096 7bxdtxb2niicuigfdrobojrsashnlwid6/jA9puDY80`
5. Create a file you wish to transfer and convert its contents to bytes
- To keep things simple `SGVsbG8gV29ybGQ=` should be de-serialize to the sentence "Hello World", you can use this as your file contents
6. Create your Request Body:
```
{
"responseItems": [
{
"returnQueueConsumerConfig": {
"queueName": "LCU.Queue.FTP",
"exchangeName": "LCU.FTP.Direct",
"subject": "LCU.Queue.FTP"
},
"returnQueueConnectionConfig": {
"Url" : "<QA Rabbitmq Server>",
"Port" : <QA Rabbit MQ Port>,
"Username" : "<FTP Username>",
"Password" : "<FTP Password>"
},
"ResponsePayload":"{\"test\":\"response\"}"
}
],
"protocol": 0,
"hostName": "<FirstnameLastname.lcu-internal.com>",
"portNumber": 22,
"userName": "demo",
"password": "demo",
"sshHostKeyFingerprint": "<Replace With Your Fingerprint>",
"remoteFilePath": "/sftp/test/test.txt",
"localFileData": "SGVsbG8gV29ybGQ="
}
```
7. Post the /sftp request
8. Monitor the RabbitMq LCU.Queue.FTP queue for a spike
9. Monitor your Docker SFTP logs to see a remote login
10. Monitor SEQ for the FTP API Environment you are using to see the success or fail message
11. Verify that /sft/test/text.txt exists in your sftp server files
You can modify this process and the request body to Fein success and failure and monitor the relevant success and failure queues as well.
**Note**: If you are receiving the following errror
`System.Net.Sockets.SocketException (10061): No connection could be made because the target machine actively refused it.`
in the SEQ logs. This is not a problem with your code, but something wrong with the SFTP server you are hosting. Ensure you have no other applications running on port 22 on your machine.
+55
View File
@@ -0,0 +1,55 @@
# ASP.NET Core
name: 'FTP.$(Build.SourceBranchName).$(Date:yyyyMMdd)$(Rev:.r)'
trigger:
branches:
include:
- '*'
pool:
name: LCUOnPrem
variables:
buildConfiguration: 'Release'
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'restore'
feedsToUse: 'select'
vstsFeed: '58ebbac4-8e98-4281-bb3f-58003852f4a7'
- script: dotnet build --configuration $(buildConfiguration)
displayName: 'dotnet build $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: Test
inputs:
command: test
projects: '$(Parameters.TestProjects)'
arguments: '--configuration $(BuildConfiguration) --collect "Code coverage" --settings additional_test_args.runsettings'
- task: DotNetCoreCLI@2
displayName: 'dotnet publish $(buildConfiguration)'
inputs:
command: publish
publishWebProjects: false
arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: true
- task: NuGetCommand@2
displayName: 'NuGet push'
inputs:
command: push
packagesToPush: 'LCU.Api.Internal.FTPLib/**/*.nupkg'
publishVstsFeed: '58ebbac4-8e98-4281-bb3f-58003852f4a7'
allowPackageConflicts: true
- task: PublishBuildArtifacts@1
condition: and (succeeded(), or( eq(variables['Build.SourceBranchName'], 'develop'), eq(variables['Build.SourceBranchName'], 'master')) )
displayName: 'Publish Artifact: dist'
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)'
ArtifactName: 'dist'