using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.IO; using System.IO.Compression; using System.Linq; using LeafWeb.Core.Utility; namespace LeafWeb.Core.Entities { public class LeafInput : ILeafInput { public int Id { get; set; } public virtual ICollection InputFiles { get; set; } public virtual ICollection LeafInputData { get; set; } public virtual ICollection OutputFiles { get; set; } [Required(ErrorMessage = "Name required")] public string Name { get; set; } [Required(ErrorMessage = "An email address is required")] public string Email { get; set; } [Required(ErrorMessage = "A unique identifier is required")] public string Identifier { get; set; } [Required(ErrorMessage = "Site Id required")] public string SiteId { get; set; } [Required] [Column(TypeName = "VARCHAR")] [StringLength(12)] [Index("IX_UniqueToken", 1, IsUnique = true)] public string UniqueToken { get; set; } // [Required(ErrorMessage = "PhotosynthesisType required")] // http://stackoverflow.com/questions/6038541/ef-validation-failing-on-update-when-using-lazy-loaded-required-properties public virtual PhotosynthesisType PhotosynthesisType { get; set; } [DataType(DataType.Date)] [Required] public DateTime Added { get; set; } public LeafInputStatusType CurrentStatus { get; set; } public virtual ICollection StatusHistory { get; set; } public Priority PendingPriority { get; set; } #region Calculated properties public LeafOutputFile OutputErrorMessage => OutputFiles?.FirstOrDefault(f => f.IsErrorMessage); public LeafOutputFile OutputWarningMessage => OutputFiles?.FirstOrDefault(f => f.IsWarningMessage); public bool HasOutputFiles => OutputFiles.Any(); public bool HasLeafChart => OutputFiles.Any(f => f.IsLeafChartFile); public bool IsPending => CurrentStatus == LeafInputStatusType.Pending; public bool IsStarting => CurrentStatus == LeafInputStatusType.Starting; public bool IsRunning => CurrentStatus == LeafInputStatusType.Running; public bool IsFinishing => CurrentStatus == LeafInputStatusType.Finishing; public bool IsComplete => CurrentStatus == LeafInputStatusType.Complete; public bool IsException => CurrentStatus == LeafInputStatusType.Exception; public bool IsCancelPending => CurrentStatus == LeafInputStatusType.CancelPending; public bool IsCancelling => CurrentStatus == LeafInputStatusType.Cancelling; public bool IsCancelled => CurrentStatus == LeafInputStatusType.Cancelled; public bool IsUnresponsive => CurrentStatus == LeafInputStatusType.Unresponsive; public bool IsInProgress => IsStarting || IsRunning || IsFinishing || IsCancelPending || IsCancelling || IsUnresponsive; public bool IsCancellable => IsRunning || IsPending || IsUnresponsive; public bool IsDeletable => !IsInProgress; public bool IsAtEndState => IsComplete || IsException || IsCancelled; public DateTime? StartTime { get { if (IsPending) { return null; } return StatusHistory.FirstOrDefault(s => s.Status == LeafInputStatusType.Starting)?.DateTime; } } public DateTime? EndTime { get { if (!IsAtEndState) { return null; } return StatusHistory.FirstOrDefault( s => s.Status == LeafInputStatusType.Complete || s.Status == LeafInputStatusType.Exception || s.Status == LeafInputStatusType.Cancelled)?.DateTime; } } public DateTime LastStatusChange => StatusHistory.Last().DateTime; public TimeSpan TimeInProgress { get { var start = StartTime; if (!start.HasValue) { return TimeSpan.Zero; } var end = EndTime; if (!end.HasValue) { return DateTime.Now - start.Value; } return end.Value - start.Value; } } #endregion public override string ToString() { return $"{Id}_{Identifier}"; } #region Zip /// /// Contains all output files in a zip /// public byte[] GetOutputFileZip(LeafOutputFileType? fileType) { return NewMemoryZipArchive((archive, stream) => CompressOutputFiles(archive, OutputFiles.Where(f => !fileType.HasValue || f.FileType == fileType.Value))); } /// /// Contains all input files in a zip /// public byte[] GetInputFileZip() => NewMemoryZipArchive((archive, stream) => CompressInputFiles(archive, InputFiles)); /// /// Contains all input files files in a zip, ordered into directories /// public static byte[] GetInputFilesZip(IEnumerable leafInputs) => GetLeafFilesZip( leafInputs, (archive, input, directory) => CompressInputFiles(archive, input.InputFiles, directory)); /// /// Contains all output files files in a zip, ordered into directories /// public static byte[] GetOutputFilesZip_ToUser(IEnumerable leafInputs) => GetLeafFilesZip( leafInputs, (archive, input, directory) => CompressOutputFiles(archive, input.OutputFiles .Where(f => f.FileType == LeafOutputFileType.ToUser), directory)); private static byte[] GetLeafFilesZip(IEnumerable leafInputs, Action compressAction) => NewMemoryZipArchive((archive, stream) => { var inputs = leafInputs.ToList(); var leftBehindFiles = new List(); foreach (var leafInput in inputs) { if (stream.Length > 2560000L) { leftBehindFiles.Add(leafInput.Identifier.FilterValidFilename()); continue; } var directory = $"{leafInput.Added:yyyy-dd-MM--HH-mm}_{leafInput.Identifier.FilterValidFilename()}"; compressAction(archive, leafInput, directory); } if (leftBehindFiles.Count > 0) { leftBehindFiles.Insert(0, "NOTICE. The following LeafInput were not included due to file size issue."); leftBehindFiles.Insert(1, "---"); CompressStringToFile(archive, leftBehindFiles.Join(Environment.NewLine), "README.TXT"); } }); private static byte[] NewMemoryZipArchive(Action addFiles) { using (var compressedFileStream = new MemoryStream()) { using (var archive = new ZipArchive(compressedFileStream, ZipArchiveMode.Create, true)) { addFiles(archive, compressedFileStream); } // ZipArchive must be disposed before returning the data return compressedFileStream.ToArray(); } } private static long CompressInputFiles(ZipArchive archive, IEnumerable inputFiles, string directory = "") { var contentsLength = 0L; foreach (var inputFile in inputFiles) { contentsLength += inputFile.Contents.LongLength; var entry = archive.CreateEntry(Path.Combine(directory, inputFile.Filename)); using (var originalFileStream = new MemoryStream(inputFile.Contents)) using (var entryStream = entry.Open()) originalFileStream.CopyTo(entryStream); } return contentsLength; } private static long CompressOutputFiles(ZipArchive archive, IEnumerable outputFiles, string directory = "") { var contentsLength = 0L; foreach (var outputFile in outputFiles) { contentsLength += outputFile.FileContents.Contents.LongLength; var entry = archive.CreateEntry(Path.Combine(directory, outputFile.Filename)); using (var originalFileStream = new MemoryStream(outputFile.FileContents.Contents)) using (var entryStream = entry.Open()) originalFileStream.CopyTo(entryStream); } return contentsLength; } private static void CompressStringToFile(ZipArchive archive, string contents, string filename) { var entry = archive.CreateEntry(filename); using (var writer = new StreamWriter(entry.Open())) { writer.Write(contents); } } public int GetOutputFileSizeSum() { return OutputFiles.Sum(o => o.FileContents.Contents.Length); } #endregion } }