diff --git a/Core.Tests/Utility/StringExtensionsTests.cs b/Core.Tests/Utility/StringExtensionsTests.cs index c7652dc..6478009 100644 --- a/Core.Tests/Utility/StringExtensionsTests.cs +++ b/Core.Tests/Utility/StringExtensionsTests.cs @@ -39,6 +39,22 @@ namespace LeafWeb.Core.Tests.Utility Assert.That(result, Is.EqualTo("newline")); } + [Test] + public void FilterAlphaNumeric_Apostrophes() + { + var str = "apostrophe's"; + var result = str.FilterAlphaNumeric(); + Assert.That(result, Is.EqualTo("apostrophes")); + } + + [Test] + public void FilterAlphaNumeric_ValidFilename() + { + var str = "my apostrophe's.csv"; + var result = str.FilterValidFilename(); + Assert.That(result, Is.EqualTo("myapostrophes.csv")); + } + [Test] public void FilenameFromPath() { @@ -46,5 +62,13 @@ namespace LeafWeb.Core.Tests.Utility var result = str.FilenameFromPath(); Assert.That(result, Is.EqualTo("file.ext")); } + + [Test] + public void WhitespaceToUnderscoreTest() + { + var str = " w h i t e s p " + Environment.NewLine + " a c e"; + var result = str.WhitespaceToUnderscore(); + Assert.That(result, Is.EqualTo("_w_h_i_t_e_s_p_a_c_e")); + } } } diff --git a/Core/DAL/DataService.cs b/Core/DAL/DataService.cs index 55decd1..ba4f43e 100644 --- a/Core/DAL/DataService.cs +++ b/Core/DAL/DataService.cs @@ -69,6 +69,11 @@ namespace LeafWeb.Core.DAL return _db.LeafInputFiles; } + public LeafInputFile GetLeafInputFile(int id) + { + return _db.LeafInputFiles.Find(id); + } + public IQueryable GetLeafInputFiles(LeafInputStatusType status) { return diff --git a/Core/Remote/PiscalLeafInputFile.cs b/Core/Remote/PiscalLeafInputFile.cs index 422d41e..fa27843 100644 --- a/Core/Remote/PiscalLeafInputFile.cs +++ b/Core/Remote/PiscalLeafInputFile.cs @@ -1,5 +1,6 @@ using AutoMapper; using LeafWeb.Core.Entities; +using LeafWeb.Core.Utility; namespace LeafWeb.Core.Remote { @@ -11,6 +12,7 @@ namespace LeafWeb.Core.Remote public string Filename { get; set; } public byte[] Contents { get; set; } public string DirectoryName { get; set; } + public string PhotosyntheticType { get; set; } static PiscalLeafInputFile() { @@ -18,9 +20,14 @@ namespace LeafWeb.Core.Remote new MapperConfiguration(cfg => { cfg.CreateMap() - .ForMember(dest => dest.DirectoryName, opt => opt.MapFrom( - src => PiscalUtility.GetPiscalDirectoryName(src))) - .ForMember(dest => dest.LeafInputId, opt => opt.MapFrom(src => src.Id)); + .ForMember(dest => dest.DirectoryName, + opt => opt.MapFrom(src => PiscalUtility.GetPiscalDirectoryName(src))) + .ForMember(dest => dest.Filename, opt => + opt.MapFrom(src => src.Filename.WhitespaceToUnderscore().FilterValidFilename())) + .ForMember(dest => dest.LeafInputId, opt => opt.MapFrom(src => src.Id)) + .ForMember( + dest => dest.PhotosyntheticType, + opt => opt.MapFrom(src => src.LeafInput.PhotosynthesisType.Id.WhitespaceToUnderscore())); }); Mapper = config.CreateMapper(); } diff --git a/Core/Remote/PiscalSshClient.cs b/Core/Remote/PiscalSshClient.cs index 5db655c..fdb6730 100644 --- a/Core/Remote/PiscalSshClient.cs +++ b/Core/Remote/PiscalSshClient.cs @@ -57,7 +57,7 @@ namespace LeafWeb.Core.Remote using (var ssh = GetSshClient()) { ssh.Connect(); - var commandText = $"{RemoteScriptPath} -d {file.DirectoryName} -f {file.Filename}"; + var commandText = $"{RemoteScriptPath} -d {file.DirectoryName} -f {file.Filename} -p {file.PhotosyntheticType}"; var command = ssh.CreateCommand(commandText); command.Execute(); ssh.Disconnect(); diff --git a/Core/Utility/StringExtensions.cs b/Core/Utility/StringExtensions.cs index 6027e4f..cc71337 100644 --- a/Core/Utility/StringExtensions.cs +++ b/Core/Utility/StringExtensions.cs @@ -32,6 +32,11 @@ namespace LeafWeb.Core.Utility return char.ToLowerInvariant(s[0]) + s.Substring(1); } + public static string WhitespaceToUnderscore(this string str) + { + return Regex.Replace(str, @"\s+", "_"); + } + public static byte[] GetBytes(this string str) { return System.Text.Encoding.Default.GetBytes(str); @@ -47,6 +52,11 @@ namespace LeafWeb.Core.Utility return Regex.Replace(input, @"[^\w_]", ""); } + public static string FilterValidFilename(this string input) + { + return Regex.Replace(input, @"[^\w_\-\.]", ""); + } + public static string FilenameFromPath(this string path) { return Regex.Replace(path, @".*/([^/]*$)", "$1"); diff --git a/Web/NLog.config b/Web/NLog.config index e044f2e..dbc8f5e 100644 --- a/Web/NLog.config +++ b/Web/NLog.config @@ -17,7 +17,7 @@ - + \ No newline at end of file diff --git a/Web/Services/EmailNotificationService.cs b/Web/Services/EmailNotificationService.cs new file mode 100644 index 0000000..9c0a42b --- /dev/null +++ b/Web/Services/EmailNotificationService.cs @@ -0,0 +1,93 @@ +using System; +using System.Configuration; +using System.IO; +using System.Linq; +using System.Net.Mail; +using LeafWeb.Core.DAL; +using LeafWeb.Core.Entities; +using NLog; + +namespace LeafWeb.Web.Services +{ + public class EmailNotificationService : IDisposable + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private readonly string _emaialFromAddress; + + private readonly SmtpClient _smtpClient; + + private readonly DataService _dataService; + + public EmailNotificationService(DataService dataService) + { + _dataService = dataService; + + _smtpClient = new SmtpClient(); + + _emaialFromAddress = ConfigurationManager.AppSettings["EmailFromAddress"]; + } + + public EmailNotificationService() : this(new DataService()) + { } + + public void SendLeafWebError(int leafWebFileId, string errorMessage) + { + var file = _dataService.GetLeafInputFile(leafWebFileId); + var body = $"Your LeafWeb analysis job, {file.LeafInput.Identifier} (with filename {file.Filename}), encountered the following errors." + Environment.NewLine + + "You will need to correct your input and resubmit." + Environment.NewLine + Environment.NewLine + + errorMessage; + var message = new MailMessage(_emaialFromAddress, file.LeafInput.Email, "LeafWeb processing error", body); + SendMessage(message); + } + + public void SendLeafWebSuccess(int leafWebFileId) + { + var file = _dataService.GetLeafInputFile(leafWebFileId); + var body = $"Your LeafWeb analysis job, {file.LeafInput.Identifier} (with filename {file.Filename}), has completed." + Environment.NewLine; + var message = new MailMessage(_emaialFromAddress, file.LeafInput.Email, "LeafWeb results", body); + + var fileStreams = + (from outputFile in + file.LeafOutputFiles + select Tuple.Create(outputFile, new MemoryStream(outputFile.Contents))).ToList(); + + try + { + foreach (var fileStream in fileStreams) + { + var attachment = new Attachment(fileStream.Item2, fileStream.Item1.Filename); + message.Attachments.Add(attachment); + } + + SendMessage(message); + } + finally + { + // can't dispose those memory streams until the message is sent + foreach (var stream in fileStreams.Select(f => f.Item2)) + { + stream.Dispose(); + } + } + } + + private void SendMessage(MailMessage mailMessage) + { + try + { + Logger.Debug("Email sending to " + mailMessage.To + ", subject: " + mailMessage.Subject); + _smtpClient.Send(mailMessage); + } + catch (SmtpException ex) + { + Logger.Error(ex, "Failed to send mail: {0}", ex.Message); + } + } + + public void Dispose() + { + _dataService.Dispose(); + } + } +} \ No newline at end of file diff --git a/Web/Services/PiscalQueueManager.cs b/Web/Services/PiscalQueueManager.cs index d736375..9e09dd7 100644 --- a/Web/Services/PiscalQueueManager.cs +++ b/Web/Services/PiscalQueueManager.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading; +using Hangfire; using LeafWeb.Core.DAL; using LeafWeb.Core.Entities; using LeafWeb.Core.Remote; @@ -12,14 +13,16 @@ namespace LeafWeb.Web.Services { private readonly DataService _dataService; private readonly PiscalService _piscalService; + private readonly EmailNotificationService _emailService; - public PiscalQueueManager(DataService dataService, PiscalService piscalService) + public PiscalQueueManager(DataService dataService, PiscalService piscalService, EmailNotificationService emailService) { _dataService = dataService; _piscalService = piscalService; + _emailService = emailService; } - public PiscalQueueManager() : this(new DataService(), new PiscalService()) {} + public PiscalQueueManager() : this(new DataService(), new PiscalService(), new EmailNotificationService()) {} private static readonly object ProcessQueueLock = new object(); @@ -109,6 +112,8 @@ namespace LeafWeb.Web.Services // update db _dataService.SetLeafInputFileStatus(file, LeafInputStatusType.Complete); + BackgroundJob.Enqueue(() => _emailService.SendLeafWebSuccess(file.Id)); + // remove working data from the server logger.Info("LeafInputFile: {0}, Cleanup", file.Id); _piscalService.Cleanup(file); @@ -127,6 +132,12 @@ namespace LeafWeb.Web.Services logger.Info("LeafInputFile: {0}, Error Message: {1}", file.Id, errorMessage); _dataService.SetLeafInputFileStatus(file, LeafInputStatusType.Error, errorMessage); + + BackgroundJob.Enqueue(() => _emailService.SendLeafWebError(file.Id, errorMessage)); + + // remove working data from the server + logger.Info("LeafInputFile: {0}, Cleanup", file.Id); + _piscalService.Cleanup(file); break; } } diff --git a/Web/Web.config b/Web/Web.config index 5bef049..c843521 100644 --- a/Web/Web.config +++ b/Web/Web.config @@ -22,8 +22,16 @@ - - + + + + + + + + + + diff --git a/Web/Web.csproj b/Web/Web.csproj index f1383d8..abfe585 100644 --- a/Web/Web.csproj +++ b/Web/Web.csproj @@ -933,6 +933,7 @@ +