using BO.ProcessServer.Options; using Quadarax.Foundation.Core.Object; using System; using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; using Quadarax.Foundation.Core.IO.Extensions; using Quadarax.Foundation.Core.Logging; // ReSharper disable once InconsistentNaming namespace BO.ProcessServer.Business { internal class Pool : DisposableObject { #region *** Enums *** public enum PoolTypeEnum { Pending, Working, Done, Fail } #endregion #region *** Constants *** private const string CS_ALL_FILES = "*.*"; private const string CS_ALL_FILES_WOEXT = "*"; #endregion #region *** Private fields *** private readonly IFileSystem _fileSystem; private IDirectoryInfo _dirPending; private IDirectoryInfo _dirWorking; private IDirectoryInfo _dirDone; private IDirectoryInfo _dirFail; private ILog _log; #endregion #region *** Constructors *** public Pool(Configuration configuration, IFileSystem fileSystem, ILogger logger) { _log = logger.GetLogger(GetType()); if (configuration==null) throw new ArgumentNullException(nameof(configuration)); _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); var basePath = TranslatePath(configuration.System.PoolPathBase); if (!_fileSystem.Directory.Exists(TranslatePath(configuration.System.PoolPathBase))) throw new FileNotFoundException($"BasePool directory '{basePath}' not found!"); _dirPending = _fileSystem.DirectoryInfo.FromDirectoryName(_fileSystem.Path.Combine(basePath, configuration.System.PoolPathPending)); _dirWorking = _fileSystem.DirectoryInfo.FromDirectoryName(_fileSystem.Path.Combine(basePath, configuration.System.PoolPathWorking)); _dirDone = _fileSystem.DirectoryInfo.FromDirectoryName(_fileSystem.Path.Combine(basePath, configuration.System.PoolPathDone)); _dirFail = _fileSystem.DirectoryInfo.FromDirectoryName(_fileSystem.Path.Combine(basePath, configuration.System.PoolPathFail)); _log.Log(LogSeverityEnum.Info, $"Open pool in '{basePath}'. Initial status (files): Pending={_dirPending.GetFiles().Length};Working={_dirWorking.GetFiles().Length};Done={_dirDone.GetFiles().Length};Fail={_dirFail.GetFiles().Length}"); } #endregion #region *** Public Operations *** public async Task PutDocumentAsync(long documentId, string fileName, Stream streamToWrite) { var poolFileName = TranslateFileName(documentId, fileName); if (_dirPending.ExistsFile(poolFileName)) { _log.Log(LogSeverityEnum.Warn, $"Pool [Pending]: File '{poolFileName}' already exists. Overwrite."); _dirPending.DeleteFile(poolFileName); } var fullFileName = _dirPending.GetFullFileName(poolFileName); await using (var sw = _fileSystem.File.OpenWrite(fullFileName)) { streamToWrite.Seek(0, SeekOrigin.Begin); await streamToWrite.CopyToAsync(sw); await sw.FlushAsync(); _log.Log(LogSeverityEnum.Debug, $"Pool [Pending]: File '{poolFileName}' created."); } return fullFileName; } public async Task PopDocumentAsync(string fullFileName) { var memory = new MemoryStream(); await using (var sw = _fileSystem.File.OpenRead(fullFileName)) { await sw.CopyToAsync(memory); await memory.FlushAsync(); _log.Log(LogSeverityEnum.Debug, $"Pool: File '{fullFileName}' pop."); } memory.Seek(0, SeekOrigin.Begin); return memory; } public async Task>> GetSinglePendingDocumentFilesAsync() { var first = GetPoolBranch(PoolTypeEnum.Pending).EnumerateFiles(CS_ALL_FILES, SearchOption.TopDirectoryOnly).FirstOrDefault(); if (first == null) return new Tuple>(-1, new List()); var documentId = GetDocumentIdFromFileName(first.Name); return new Tuple>(documentId, GetDocumentFiles(PoolTypeEnum.Pending, documentId)); } public long GetDocumentIdFromFileName(string fileName) { var fileNameOnly = _fileSystem.Path.GetFileName(fileName); var pos = fileNameOnly.IndexOf("_", StringComparison.InvariantCulture); if (pos < 0) throw new ArgumentOutOfRangeException(nameof(fileName), $"FileName '{fileName}' format is unexpected (00000000_name.ext expects)."); return long.Parse(fileNameOnly.Substring(0, pos)); } public int GetDocumentsCount(PoolTypeEnum poolBranchFrom) { return GetPoolBranch(poolBranchFrom).GetFiles(CS_ALL_FILES, SearchOption.TopDirectoryOnly).Select(x=>x.FullName).Count(); } public long GetDocumentsSize(PoolTypeEnum poolBranchFrom) { var files = GetPoolBranch(poolBranchFrom).GetFiles(CS_ALL_FILES, SearchOption.TopDirectoryOnly); return files.Aggregate(0, (current, file) => current + file.Length); } public IEnumerable GetDocumentIdsFromOldest(PoolTypeEnum poolBranchFrom) { var result = new List(); var files = GetPoolBranch(poolBranchFrom).GetFiles(CS_ALL_FILES, SearchOption.TopDirectoryOnly).OrderByDescending(x=>x.LastWriteTime); foreach (var file in files) { var docId = GetDocumentIdFromFileName(file.FullName); if (!result.Contains(docId)) result.Add(docId); } return result; } public IEnumerable GetDocumentFiles(PoolTypeEnum poolBranchFrom, long documentId, string alternativeExt = null) { return GetPoolBranch(poolBranchFrom).GetFiles(TranslateSearchPattern(documentId,alternativeExt), SearchOption.TopDirectoryOnly).Select(x=>x.FullName); } public IEnumerable MoveDocumentFiles(PoolTypeEnum poolBranchFrom, PoolTypeEnum poolBranchTo, long documentId, string alternativeExt = null) { var files = GetDocumentFiles(poolBranchFrom, documentId, alternativeExt).ToArray(); var tasks = new List(); foreach (var file in files) { tasks.Add(Task.Factory.StartNew(() => { _fileSystem.File.Move(file, GetPoolBranch(poolBranchTo).GetFullFileName(_fileSystem.Path.GetFileName(file))); _log.Log(LogSeverityEnum.Debug, $"Pool [{poolBranchFrom}]: File '{_fileSystem.Path.GetFileName(file)}' moved to [{poolBranchTo}]."); } )); } Task.WaitAll(tasks.ToArray()); return files; } public IEnumerable DeleteDocumentFiles(PoolTypeEnum poolBranchFrom, long documentId) { var files = GetPoolBranch(poolBranchFrom).GetFiles(TranslateSearchPattern(documentId), SearchOption.TopDirectoryOnly); foreach (var file in files) { file.Delete(); _log.Log(LogSeverityEnum.Debug, $"Pool [{poolBranchFrom}]: File '{file.Name}' deleted."); } return files.Select(x => x.FullName); } #endregion #region *** Private operations & overrides *** protected override void OnDisposing() { _dirPending = null; _dirWorking = null; _dirDone = null; _dirFail = null; } private string TranslatePath(string path) { if (string.IsNullOrEmpty(path)) return path; return path.Replace("{CD}", _fileSystem.Directory.GetCurrentDirectory()); } private string TranslateFileName(long documentId, string originalFileName) { return $"{documentId:D8}_{originalFileName}"; } private string TranslateSearchPattern(long documentId,string alternativeExt = null) { var search = CS_ALL_FILES; if (!string.IsNullOrEmpty(alternativeExt)) search = CS_ALL_FILES_WOEXT + alternativeExt; return $"{documentId:D8}_{CS_ALL_FILES}"; } private IDirectoryInfo GetPoolBranch(PoolTypeEnum poolBranch) { switch (poolBranch) { case PoolTypeEnum.Done: return _dirDone; case PoolTypeEnum.Fail: return _dirFail; case PoolTypeEnum.Pending: return _dirPending; case PoolTypeEnum.Working: return _dirWorking; default: throw new NotSupportedException(); } } #endregion } }