Forráskód Böngészése

Add StorageService, Add DocumentService. Put together in ConsoleController. (Console.Connection is not done yet)

Dalibor Votruba 4 éve
szülő
commit
22a8154da1

+ 1 - 0
AppServer/Business/Business.csproj

@@ -13,6 +13,7 @@
     <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.8" />
     <PackageReference Include="Microsoft.Extensions.Identity.Core" Version="5.0.8" />
+    <PackageReference Include="System.IO.Abstractions" Version="13.2.43" />
   </ItemGroup>
 
   <ItemGroup>

+ 9 - 0
AppServer/Business/Configuration/Configuration.cs

@@ -25,6 +25,7 @@ namespace BO.AppServer.Business.Configuration
             public CfgSystemAccount AccountConsole { get; set; }
             public CfgSystemAccount AccountAdmin { get; set; }
             public string SessionIdleTimeout { get; set; }
+            public CfgSystemStorage Storage { get; set; }
         }
 
         public class CfgSystemAccount
@@ -40,5 +41,13 @@ namespace BO.AppServer.Business.Configuration
         {
             public string DefaultBillingPlan { get; set; }
         }
+
+        public class CfgSystemStorage
+        {
+            public string InputPath { get; set; }
+            public string OutputPath { get; set; }
+            public int IoBufferSize { get; set; }
+        }
     }
+
 }

+ 10 - 3
AppServer/Business/ErrorCodes.txt

@@ -6,11 +6,18 @@ Business (1000 - 2000)
 1001	Entity '{0}' named '{1}' not exists in database.
 1010	Entity '{0}' filed '{1}' maximal length overflow (max. {2})
 
-Business.UserService (1100 - 1200)
+Business.UserService (1100 - 1199)
 1100	Cannot create user '{0}', user name or email already exists.
 
-Business.AccessService (1200 - 1300)
+Business.AccessService (1200 - 1299)
 1200	User '{0}' has no permission to login in. Access denied!
 1201	User '{0}' doesn't exists!
 1202	User '{0}' is locked out!
-1203	Invalid login attempt for user '{userName}'.
+1203	Invalid login attempt for user '{userName}'.
+
+
+Business.StorageService (1300 - 1399)
+1300	Content for artifact '{0}' [{1}] not found!
+
+Business.WorkspaceService (1400 - 1499)
+1400	Workspace with APIKey '{0}' not exists or API password is not valid!

+ 239 - 0
AppServer/Business/Services/DocumentService.cs

@@ -0,0 +1,239 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security.Principal;
+using System.Threading.Tasks;
+using BO.AppServer.Business.Exceptions;
+using BO.AppServer.Business.Mapper;
+using BO.AppServer.Business.Services.Base;
+using BO.AppServer.Data.Entity;
+using BO.AppServer.Data.Repository;
+using BO.AppServer.Metadata.Dto;
+using BO.AppServer.Metadata.Enums;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Logging;
+using Quadarax.Foundation.Core.Data;
+using Quadarax.Foundation.Core.Data.Interface;
+using Quadarax.Foundation.Core.Data.Interface.Entity.Dto;
+
+namespace BO.AppServer.Business.Services
+{
+    public class DocumentService : UserContextService
+    {
+        #region *** Private fields ***
+        private WorkspaceRepo _repoWorkspace;
+        private MetadocumentRepo _repoDocument;
+        private MimeTypeRepo _repoMType;
+        private ArtifactRepo _repoArtifact;
+
+        private StorageService _srvStorage;
+        #endregion
+
+        #region *** Constructors ***
+        public DocumentService(
+            WorkspaceRepo repoWorkspace,
+            MetadocumentRepo repoDocument,
+            MimeTypeRepo repoMType,
+            ArtifactRepo repoArtifact,
+            UserRepo repoUser, 
+            UserManager<IdentityUser> userManager, 
+            IPrincipal currentPrincipal, 
+            ILoggerFactory logger) : base(repoUser, userManager, currentPrincipal, logger)
+        {
+            _repoWorkspace = repoWorkspace ?? throw new ArgumentNullException(nameof(repoWorkspace));
+            _repoDocument = repoDocument ?? throw new ArgumentNullException(nameof(repoDocument));
+            _repoMType = repoMType ?? throw new ArgumentNullException(nameof(repoMType));
+            _repoArtifact = repoArtifact ??  throw new ArgumentNullException(nameof(repoArtifact));
+        }
+        #endregion
+
+        #region *** Public Operations ***
+
+        public async Task<ResultValueDto<DocumentRDto>> GetDocumentAsync(long workspaceId, long documentId)
+        {
+            var document = CheckAndGetDocument(workspaceId, documentId);
+            return new ResultValueDto<DocumentRDto>(document.Map<Metadocument, DocumentRDto>());
+
+        }
+
+        public async Task<ResultsValueDto<DocumentRDto>> GetAllDocumentsAsync(long workspaceId,  DocumentsScopeEnums scope, PagingDto paging)
+        {
+            CheckAndGetWorkspace(workspaceId);
+            IList<Metadocument> documents = new List<Metadocument>();
+            switch (scope)
+            {
+                case DocumentsScopeEnums.All:
+                    documents = _repoDocument.Query(paging).Where(x => x.Structure.WorkspaceId == workspaceId).OrderBy(x => x.Name).ToList();
+                    break;
+                case DocumentsScopeEnums.Pending:
+                    documents = _repoDocument.Query(paging).Where(x => x.Structure.WorkspaceId == workspaceId && x.Status == (int)DocumentStatusEnum.Pending).OrderBy(x => x.Name).ToList();
+                    break;
+                case DocumentsScopeEnums.Done:
+                    documents = _repoDocument.Query(paging).Where(x => x.Structure.WorkspaceId == workspaceId && x.Status == (int)DocumentStatusEnum.DoneOk).OrderBy(x => x.Name).ToList();
+                    break;
+                case DocumentsScopeEnums.Working:
+                    documents = _repoDocument.Query(paging).Where(x => x.Structure.WorkspaceId == workspaceId && x.Status == (int)DocumentStatusEnum.Processing).OrderBy(x => x.Name).ToList();
+                    break;
+                case DocumentsScopeEnums.Failed:
+                    documents = _repoDocument.Query(paging).Where(x => x.Structure.WorkspaceId == workspaceId && x.Status == (int)DocumentStatusEnum.DoneFail).OrderBy(x => x.Name).ToList();
+                    break;
+            }
+
+            return new ResultsValueDto<DocumentRDto>(documents.MapList<Metadocument, DocumentRDto>());
+        }
+
+        public async Task<ResultsValueDto<DocumentRDto>> GetAllDocumentsAsync(DocumentsScopeEnums scope, PagingDto paging)
+        {
+            IList<Metadocument> documents = new List<Metadocument>();
+            switch (scope)
+            {
+                case DocumentsScopeEnums.All:
+                    documents = _repoDocument.Query(paging).OrderBy(x => x.Name).ToList();
+                    break;
+                case DocumentsScopeEnums.Pending:
+                    documents = _repoDocument.Query(paging).Where(x => x.Status == (int)DocumentStatusEnum.Pending).OrderBy(x => x.Name).ToList();
+                    break;
+                case DocumentsScopeEnums.Done:
+                    documents = _repoDocument.Query(paging).Where(x => x.Status == (int)DocumentStatusEnum.DoneOk).OrderBy(x => x.Name).ToList();
+                    break;
+                case DocumentsScopeEnums.Working:
+                    documents = _repoDocument.Query(paging).Where(x => x.Status == (int)DocumentStatusEnum.Processing).OrderBy(x => x.Name).ToList();
+                    break;
+                case DocumentsScopeEnums.Failed:
+                    documents = _repoDocument.Query(paging).Where(x => x.Status == (int)DocumentStatusEnum.DoneFail).OrderBy(x => x.Name).ToList();
+                    break;
+            }
+
+            return new ResultsValueDto<DocumentRDto>(documents.MapList<Metadocument, DocumentRDto>());
+        }
+
+        public async Task<ResultValueDto<DocumentRDto>> CreateDocumentAsync(long workspaceId,  DocumentCDto document)
+        {
+            var workspace = CheckAndGetWorkspace(workspaceId);
+            var exists = _repoDocument.Query(new Paging())
+                .Any(x => x.Structure.WorkspaceId == workspaceId && x.Name == document.Name);
+            if (exists)
+            {
+                var exc = new EntityAlreadyExistsException(typeof(Metadocument), document.Name);
+                Log.LogWarning(exc.Message);
+                throw exc;
+            }
+
+            var newDocument = _repoDocument.New();
+            document.CopyToDto(newDocument);
+            newDocument.Created = DateTime.Now;
+            newDocument.CreatedByUser = GetCurrentUser();
+            newDocument.Status = (int)DocumentStatusEnum.New;
+            newDocument.StructureId = workspace.Structures.First().Id;
+            _repoDocument.Set(newDocument);
+            _repoDocument.Commit();
+
+            return new ResultValueDto<DocumentRDto>(newDocument.Map<Metadocument, DocumentRDto>());
+        }
+
+        public async Task<ResultValueDto<ArtifactRDto>> AppendArtifactAsync(long workspaceId, long documentId,
+            ArtifactCDto artifact, ArtifactTypeEnum type, Stream content)
+        {
+            //TODO: Rewrite better way - mainly rollback scenario
+
+            var document = CheckAndGetDocument(workspaceId, documentId);
+            var exists = document.Artifacts.Any(x =>
+                x.Name.ToLower() == artifact.Name.ToLower() && x.Extension.ToLower() == artifact.Extension.ToLower());
+            if (exists)
+            {
+                var exc = new EntityAlreadyExistsException(typeof(Artifact), artifact.Name + "." + artifact.Extension);
+                Log.LogWarning(exc.Message);
+                throw exc;
+            }
+
+            var mtype = _repoMType.GetByMimeType(artifact.MimeType);
+            if (mtype == null)
+            {
+                var exc = new EntityNotExistsException(typeof(MimeType), artifact.MimeType, 0);
+                Log.LogWarning(exc.Message);
+                throw exc;
+            }
+
+            var newArtifact = _repoArtifact.New();
+
+            newArtifact.Name = artifact.Name;
+            newArtifact.Extension = artifact.Extension;
+            newArtifact.Length = content.Length;
+            newArtifact.Metadocument = document;
+            newArtifact.MimeType = mtype;
+            newArtifact.Created = DateTime.Now;
+            newArtifact.CreatedByUser = GetCurrentUser();
+            newArtifact.Type = (int)type;
+            _repoArtifact.Set(newArtifact);
+            document = _repoDocument.Get(documentId);
+            document.Modified = DateTime.Now;
+            document.ModifiedByUser = GetCurrentUser();
+            _repoDocument.Set(document);
+            _repoArtifact.Commit();
+            _repoDocument.Commit();
+
+            await _srvStorage.PushContentAsync(newArtifact.Id, content);
+
+            return new ResultValueDto<ArtifactRDto>(newArtifact.Map<Artifact, ArtifactRDto>());
+
+
+        }
+
+        public async Task<Tuple<Stream, string>> GetArtifactAsync(long workspaceId, long documentId, long artifactId)
+        {
+            var artifact = CheckAndGetArtifact(workspaceId, documentId, artifactId);
+            var result = await _srvStorage.PopContentAsync(artifact.Id);
+
+            return new Tuple<Stream, string>(result.Item1, artifact.MimeType.Mimetype1);
+
+        }
+        #endregion
+
+        #region *** Private Operations ***
+
+        private Workspace CheckAndGetWorkspace(long workspaceId)
+        {
+            var workspace = _repoWorkspace.Get(workspaceId);
+            if (workspace == null)
+            {
+
+                var exc = new EntityNotExistsException(typeof(Workspace), nameof(Workspace.Id), workspaceId);
+                Log.LogWarning(exc.Message);
+                throw exc;
+            }
+
+            return workspace;
+        }
+
+        private Metadocument CheckAndGetDocument(long workspaceId, long documentId)
+        {
+            CheckAndGetWorkspace(workspaceId);
+            var document = _repoDocument.Get(workspaceId ,documentId);
+            if (document == null)
+            {
+
+                var exc = new EntityNotExistsException(typeof(Metadocument), nameof(Metadocument.Id), documentId);
+                Log.LogWarning(exc.Message);
+                throw exc;
+            }
+
+            return document;
+        }
+
+        private Artifact CheckAndGetArtifact(long workspaceId, long documentId, long artifactId)
+        {
+            CheckAndGetWorkspace(workspaceId);
+            CheckAndGetDocument(workspaceId, documentId);
+            var artifact = _repoDocument.GetArtifact(documentId, artifactId);
+            if (artifact == null)
+            {
+                var exc = new EntityNotExistsException(typeof(Artifact), nameof(Artifact.Id), artifactId);
+                Log.LogWarning(exc.Message);
+                throw exc;
+            }
+            return artifact;
+        }
+        #endregion
+    }
+}

+ 185 - 0
AppServer/Business/Services/StorageService.cs

@@ -0,0 +1,185 @@
+using System;
+using System.IO;
+using System.IO.Abstractions;
+using System.Security.Principal;
+using System.Text;
+using System.Threading.Tasks;
+using BO.AppServer.Business.Exceptions;
+using BO.AppServer.Data.Entity;
+using BO.AppServer.Data.Repository;
+using BO.AppServer.Metadata.Enums;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Quadarax.Foundation.Core.Business;
+using Quadarax.Foundation.Core.Exceptions;
+using Quadarax.Foundation.Core.Value.Extensions;
+
+namespace BO.AppServer.Business.Services
+{
+    public class StorageService : AbstractService
+    {
+        #region *** Private fields ***
+        private IFileSystem _fileSystem;
+        private IOptions<Configuration.Configuration> _config;
+        private ArtifactRepo _repoArtifact;
+        #endregion
+        #region *** Constructors ***
+        public StorageService(ArtifactRepo repoArtifact,
+            IOptions<Configuration.Configuration> config, 
+            IFileSystem fileSystem, 
+            IPrincipal currentPrincipal, 
+            ILoggerFactory logger) : base(currentPrincipal, logger)
+        {
+            _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+            _config = config ?? throw new ArgumentNullException(nameof(config));
+            _repoArtifact = repoArtifact ?? throw new ArgumentNullException(nameof(repoArtifact));
+
+            if (!_fileSystem.Directory.Exists(_config.Value.System.Storage.InputPath.EnsurePathNonBackslash()))
+            {
+                _fileSystem.Directory.CreateDirectory(_config.Value.System.Storage.InputPath.EnsurePathNonBackslash());
+                Log.LogInformation($"Storage Input directory '{_config.Value.System.Storage.InputPath.EnsurePathNonBackslash()}' created.");
+            }
+            else
+                Log.LogInformation($"Storage Input directory '{_config.Value.System.Storage.InputPath.EnsurePathNonBackslash()}' verified.");
+            if (!_fileSystem.Directory.Exists(_config.Value.System.Storage.OutputPath.EnsurePathNonBackslash()))
+            {
+                _fileSystem.Directory.CreateDirectory(_config.Value.System.Storage.OutputPath.EnsurePathNonBackslash());
+                Log.LogInformation($"Storage Output directory '{_config.Value.System.Storage.OutputPath.EnsurePathNonBackslash()}' created.");
+            }
+            else
+                Log.LogInformation($"Storage Input directory '{_config.Value.System.Storage.InputPath.EnsurePathNonBackslash()}' verified.");
+
+        }
+        #endregion
+        #region *** Public Operations ***
+
+        public async Task<long> PushContentAsync(long artifactId, Stream fileData)
+        {
+
+            var artifact = _repoArtifact.Get(artifactId);
+            if (artifact == null)
+            {
+                var exc = new EntityNotExistsException(typeof(Artifact),nameof(Artifact.Id), artifactId);
+                Log.LogWarning(exc.Message);
+                throw exc;
+            }
+
+            var documentId = artifact.Metadocument.Id;
+            var workspaceId = artifact.Metadocument.Structure.WorkspaceId;
+            var fileName = GetFileName(artifactId, artifact.Name, artifact.Extension);
+            var artType = (ArtifactTypeEnum)artifact.Type;
+            var fullFileName = EnureDirectory(workspaceId, documentId, artType) + _fileSystem.Path.DirectorySeparatorChar + fileName;
+            long size;
+            await using (var fs = _fileSystem.File.Create(fullFileName, _config.Value.System.Storage.IoBufferSize, FileOptions.Asynchronous))
+            {
+                fileData.Position = 0;
+                await fileData.CopyToAsync(fs, _config.Value.System.Storage.IoBufferSize);
+                await fs.FlushAsync();
+                size = fs.Length;
+                fs.Close();
+            }
+
+            return size;
+        }
+
+        public async Task<Tuple<Stream, long>> PopContentAsync(long artifactId)
+        {
+            var artifact = _repoArtifact.Get(artifactId);
+            if (artifact == null)
+            {
+                var exc = new EntityNotExistsException(typeof(Artifact),nameof(Artifact.Id), artifactId);
+                Log.LogWarning(exc.Message);
+                throw exc;
+            }
+
+            var documentId = artifact.Metadocument.Id;
+            var workspaceId = artifact.Metadocument.Structure.WorkspaceId;
+            var fileName = GetFileName(artifactId, artifact.Name, artifact.Extension);
+            var artType = (ArtifactTypeEnum)artifact.Type;
+            var fullFileName = EnureDirectory(workspaceId, documentId, artType) + _fileSystem.Path.DirectorySeparatorChar + fileName;
+
+            if (!_fileSystem.File.Exists(fullFileName))
+            {
+                var message = $"Content for artifact '{fullFileName}' [{artifactId}] not found!";
+                Log.LogWarning(message);
+                throw new CodeException(1300, message);
+            }
+
+
+            var buffer = new byte[_config.Value.System.Storage.IoBufferSize];
+
+            var outputStream = new MemoryStream(buffer);
+
+            await using (var fs = _fileSystem.File.OpenRead(fullFileName))
+            {
+                await fs.CopyToAsync(outputStream, _config.Value.System.Storage.IoBufferSize);
+                fs.Close();
+            }
+
+            return new Tuple<Stream, long>(outputStream, outputStream.Length);
+        }
+
+        public async Task<bool> DeleteContent(long artifactId)
+        {
+            var artifact = _repoArtifact.Get(artifactId);
+            if (artifact == null)
+            {
+                var exc = new EntityNotExistsException(typeof(Artifact),nameof(Artifact.Id), artifactId);
+                Log.LogWarning(exc.Message);
+                throw exc;
+            }
+
+            var documentId = artifact.Metadocument.Id;
+            var workspaceId = artifact.Metadocument.Structure.WorkspaceId;
+            var fileName = GetFileName(artifactId, artifact.Name, artifact.Extension);
+            var artType = (ArtifactTypeEnum)artifact.Type;
+            var fullFileName = EnureDirectory(workspaceId, documentId, artType) + _fileSystem.Path.DirectorySeparatorChar + fileName;
+
+            if (!_fileSystem.File.Exists(fullFileName))
+            {
+                var message = $"Content for artifact '{fullFileName}' [{artifactId}] not found for delete!";
+                Log.LogInformation(message);
+                return false;
+            }
+
+            _fileSystem.File.Delete(fullFileName);
+
+            return true;
+        }
+
+        #endregion
+
+
+        #region *** Private Operations ***
+
+        private string EnureDirectory(long workspaceId, long documentId, ArtifactTypeEnum type)
+        {
+            var dirName = GetDirectoryName(workspaceId, documentId, type);
+            if (!_fileSystem.Directory.Exists(dirName))
+            {
+                _fileSystem.Directory.CreateDirectory(dirName);
+                Log.LogInformation($"Storage document directory '{dirName}' created.");
+            }
+
+            return dirName;
+        }
+
+        private string GetDirectoryName(long workspaceId, long documentId, ArtifactTypeEnum type)
+        {
+            var sb = new StringBuilder();
+            sb.Append(type == ArtifactTypeEnum.Input ? _config.Value.System.Storage.InputPath.EnsurePathNonBackslash() : _config.Value.System.Storage.OutputPath.EnsurePathNonBackslash()).Append(_fileSystem.Path.DirectorySeparatorChar);
+            sb.Append(workspaceId).Append(_fileSystem.Path.DirectorySeparatorChar);
+            sb.Append(documentId);
+            return sb.ToString();
+        }
+        private string GetFileName(long artifactId, string fileNameWoExtension, string extension)
+        {
+            var sb = new StringBuilder();
+            sb.Append(fileNameWoExtension).Append(".");
+            sb.Append(artifactId).Append(".");
+            sb.Append(extension);
+            return sb.ToString();
+        }
+        #endregion
+    }
+}

+ 30 - 1
AppServer/Business/Services/WorkspaceService.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Security.Principal;
+using System.Threading;
 using System.Threading.Tasks;
 using BO.AppServer.Business.Exceptions;
 using BO.AppServer.Business.Mapper;
@@ -11,8 +12,10 @@ using BO.AppServer.Metadata.Dto;
 using BO.AppServer.Metadata.Enums;
 using Microsoft.AspNetCore.Identity;
 using Microsoft.Extensions.Logging;
+using Quadarax.Foundation.Core.Data;
 using Quadarax.Foundation.Core.Data.Interface;
 using Quadarax.Foundation.Core.Data.Interface.Entity.Dto;
+using Quadarax.Foundation.Core.Exceptions;
 
 namespace BO.AppServer.Business.Services
 {
@@ -58,7 +61,8 @@ namespace BO.AppServer.Business.Services
                 throw exc;
             }
 
-            var newWorkspace = _repoWorkspace.NewWorkspace(workspace.Name, billingPlan,true, owner,GetCurrentUser());
+            var current = GetCurrentUser();
+            var newWorkspace = _repoWorkspace.NewWorkspace(workspace.Name, billingPlan,true, owner,current);
             _repoWorkspace.Commit();
             return new ResultValueDto<WorkspaceRDto>(newWorkspace.Map<Workspace, WorkspaceRDto>());
         }
@@ -92,6 +96,31 @@ namespace BO.AppServer.Business.Services
             return new ResultsValueDto<WorkspaceRDto>(workspaces.MapList<Workspace, WorkspaceRDto>());
         }
 
+        public async Task<long> GetWorkspaceIdByAPIAccess(string apiKey, string apiPassword, string currentUserName)
+        {
+            var owner = string.IsNullOrEmpty(currentUserName) ? GetCurrentUser() : _repoUser.GetByName(currentUserName);
+            if (owner == null)
+            {
+                var exc = new EntityNotExistsException(typeof(User), currentUserName, 0);
+                Log.LogWarning(exc.Message);
+                throw exc;
+            }
+            if (!Guid.TryParse(apiKey, out var apiKeyGuid))
+                apiKeyGuid = Guid.Empty;
+
+            var workspace = _repoWorkspace.GetAllByOwner(owner, new Paging())
+                .FirstOrDefault(x => x.Apikey == apiKeyGuid && x.Apipassword == apiPassword);
+
+            if (workspace == null)
+            {
+                var exc = new CodeException(1400, $"Workspace with APIKey '{apiKey}' not exists or API password is not valid!");
+                Log.LogWarning(exc.Message + $"\nUser Context:'{owner.Name}' [{owner.Id}]");
+                throw exc;
+            }
+
+            return workspace.Id;
+        }
+
         #endregion
     }
 }

+ 20 - 0
AppServer/Data/Repository/MetadocumentRepo.cs

@@ -1,4 +1,6 @@
+using System.Linq;
 using BO.AppServer.Data.Entity;
+using Quadarax.Foundation.Core.Data;
 using Quadarax.Foundation.Core.Data.Domain;
 using Quadarax.Foundation.Core.Data.Interface.Domain;
 using Quadarax.Foundation.Core.Data.Repository;
@@ -8,4 +10,22 @@ public class MetadocumentRepo : EntityRepository<long,  Metadocument>
         public MetadocumentRepo(IUnitOfWork<DataDomain> unitOfWork) : base(unitOfWork)
         {
         }
+
+
+        public Metadocument Get(long workspaceId, long metadocumentId)
+        {
+            var document = Get(metadocumentId);
+            if (document == null)
+                return null;
+            if (document.Structure.WorkspaceId != workspaceId)
+                return null;
+            return document;
+
+        }
+
+        public Artifact GetArtifact(long metadocumentId, long artifactId)
+        {
+            return Query(new Paging()).Where(x => x.Id == metadocumentId)
+                .SelectMany(x => x.Artifacts).FirstOrDefault(x => x.Id == artifactId);
+        }
     }

+ 7 - 0
AppServer/Data/Repository/MimeTypeRepo.cs

@@ -1,4 +1,6 @@
+using System.Linq;
 using BO.AppServer.Data.Entity;
+using Quadarax.Foundation.Core.Data;
 using Quadarax.Foundation.Core.Data.Domain;
 using Quadarax.Foundation.Core.Data.Interface.Domain;
 using Quadarax.Foundation.Core.Data.Repository;
@@ -8,4 +10,9 @@ public class MimeTypeRepo : EntityRepository<long,  MimeType>
         public MimeTypeRepo(IUnitOfWork<DataDomain> unitOfWork) : base(unitOfWork)
         {
         }
+
+        public MimeType GetByMimeType(string mimeType)
+        {
+            return Query(new Paging()).FirstOrDefault(x => x.IsEnabled && x.Mimetype1 == mimeType.ToLower());
+        }
     }

+ 3 - 0
AppServer/Data/Repository/WorkspaceRepo.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using BO.AppServer.Data.Entity;
 using Microsoft.EntityFrameworkCore;
@@ -19,6 +20,8 @@ public class WorkspaceRepo : EntityRepository<long,  Workspace>
             var workspace = New();
             workspace.Created = DateTime.Now;
             workspace.CreatedByUser = creator;
+            if (workspace.Structures == null)
+                workspace.Structures = new List<Structure>();
             workspace.Structures.Add(new Structure(){Created = DateTime.Now,CreatedByUserId = creator.Id});
             AppendUser(workspace, owner, isDefault);
             return workspace;

+ 4 - 1
AppServer/Metadata/Dto/ArtifactDto.cs

@@ -12,8 +12,11 @@ namespace BO.AppServer.Metadata.Dto
 
     #region *** Artifact - Create ***
 
-    internal class ArtifactCDto : BoDto, ICreateDto
+    public class ArtifactCDto : IDto, ICreateDto
     {
+        public string Name { get; }
+        public string Extension { get; }
+        public string MimeType { get; }
     }
 
     #endregion

+ 5 - 4
AppServer/Metadata/Dto/DocumentDto.cs

@@ -5,18 +5,21 @@ using Quadarax.Foundation.Core.Data.Interface.Entity;
 
 namespace BO.AppServer.Metadata.Dto
 {
-    #region *** Document - Create ***
+    #region *** Document - Base ***
 
     public class DocumentBaseDto : BoDto
     {
+        public string Name { get; }
+        public DocumentStatusEnum Status { get; }
     }
 
     #endregion
 
     #region *** Document - Create ***
 
-    internal class DocumentCDto : BoDto, ICreateDto
+    public class DocumentCDto : IDto, ICreateDto
     {
+        public string Name { get; }
     }
 
     #endregion
@@ -33,7 +36,6 @@ namespace BO.AppServer.Metadata.Dto
 
     public class DocumentRDto : DocumentBaseDto, IReadDto
     {
-        public string Name { get; }
         public DateTime? LastDownloaded { get; }
         public BoAuditDto Audit { get; }
 
@@ -43,7 +45,6 @@ namespace BO.AppServer.Metadata.Dto
         public string PresetProcessProfile { get; }
         public bool PresetTest { get; }
 
-        public DocumentStatusEnum Status { get; }
         public IEnumerable<ArtifactRDto> Artifacts { get; }
         public IEnumerable<DocumentStatusHistoryRDto> History { get; }
     }

+ 81 - 1
AppServer/Web/Services/ConsoleController.cs

@@ -1,12 +1,14 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.IO;
 using System.Threading.Tasks;
 using BO.AppServer.Business;
 using BO.AppServer.Business.Services;
 using BO.AppServer.Metadata.Dto;
 using BO.AppServer.Metadata.Enums;
 using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Logging;
 using Quadarax.Foundation.Core.Data.Interface.Entity.Dto;
@@ -21,17 +23,20 @@ namespace BO.AppServer.Web.Services
         private UserService _srvUsers;
         private CatalogueService _srvCatalogue;
         private WorkspaceService _srvWorkspace;
+        private DocumentService _srvDocument;
         protected override RoleEnum LoginRoleAllowed => RoleEnum.Console;
 
         public ConsoleController(UserService srvUsers, 
             CatalogueService srvCatalogue,
             WorkspaceService srvWorkspace,
+            DocumentService srvDocument,
             ILoggerFactory logger,
             AccessService srvAccess) : base(logger, srvAccess)
         {
             _srvUsers = srvUsers ?? throw new ArgumentNullException(nameof(srvUsers));
             _srvCatalogue = srvCatalogue ?? throw new ArgumentNullException(nameof(srvCatalogue));
             _srvWorkspace = srvWorkspace ?? throw new ArgumentNullException(nameof(srvWorkspace));
+            _srvDocument = srvDocument ??  throw new ArgumentNullException(nameof(srvDocument));
         }
         
         #region *** User ***
@@ -128,7 +133,7 @@ namespace BO.AppServer.Web.Services
         #endregion
 
         #region *** Workspace ***
-        [HttpGet("{Ticket}/workspace/{scope}")]
+        [HttpGet("{Ticket}/workspaces/{scope}")]
         public async Task<ResultsValueDto<WorkspaceRDto>> GetAllWorkspaces(string ticket, WorkspaceScopeEnums scope, [FromHeader] string page, [FromHeader] string pageSize, [FromHeader] string context)
         {
             CheckAccess(ticket);
@@ -142,5 +147,80 @@ namespace BO.AppServer.Web.Services
             return await Call(async () => await _srvWorkspace.CreateWorkspaceAsync(workspace));
         }
         #endregion
+
+        #region  *** Documents ***
+        [HttpGet("{Ticket}/documents/{scope}")]
+        public async Task<ResultsValueDto<DocumentRDto>> GetAllDocuments(string ticket, DocumentsScopeEnums scope, [FromHeader] string page, [FromHeader] string pageSize)
+        {
+            CheckAccess(ticket);
+            return await Call(async () => await _srvDocument.GetAllDocumentsAsync(scope, new HeaderPagingDto(page, pageSize)));
+        }
+        
+
+        [HttpGet("{Ticket}/workspace/{wrkspApiKey}/documents/{scope}")]
+        public async Task<ResultsValueDto<DocumentRDto>> GetAllWorkspaceDocuments(string ticket, DocumentsScopeEnums scope,string wrkspApiKey,[FromHeader] string apiKeyPassword,  [FromHeader] string page, [FromHeader] string pageSize, [FromHeader] string context)
+        {
+            CheckAccess(ticket);
+            return await Call(async () =>
+            {
+                var id = await _srvWorkspace.GetWorkspaceIdByAPIAccess(wrkspApiKey, apiKeyPassword, context);
+                return await _srvDocument.GetAllDocumentsAsync(id, scope, new HeaderPagingDto(page, pageSize));
+            });
+        }
+
+        [HttpGet("{Ticket}/workspace/{wrkspApiKey}/document/{documentId}")]
+        public async Task<ResultValueDto<DocumentRDto>> GetAllWorkspaceDocument(string ticket, string wrkspApiKey,long documentId,[FromHeader] string apiKeyPassword,  [FromHeader] string context)
+        {
+            CheckAccess(ticket);
+            return await Call(async () =>
+            {
+                var id = await _srvWorkspace.GetWorkspaceIdByAPIAccess(wrkspApiKey, apiKeyPassword, context);
+                return await _srvDocument.GetDocumentAsync(id, documentId);
+            });
+        }
+
+        [HttpGet("{Ticket}/workspace/{wrkspApiKey}/document/{documentId}/artifact/{artifactId}/content")]
+        public async Task<IActionResult> GetAllWorkspaceDocumentArtifactContent(string ticket, string wrkspApiKey,long documentId,long artifactId,[FromHeader] string apiKeyPassword,  [FromHeader] string context)
+        {
+            // TODO: May wrap to better way
+            try
+            {
+                CheckAccess(ticket);
+                var id = await _srvWorkspace.GetWorkspaceIdByAPIAccess(wrkspApiKey, apiKeyPassword, context);
+                var content = await _srvDocument.GetArtifactAsync(id, documentId, artifactId);
+                return  new FileStreamResult(content.Item1, content.Item2);
+            }
+            catch (Exception e)
+            {
+                return Problem(e.Message);
+            }
+        }
+
+
+        [HttpPost("{Ticket}/workspace/{wrkspApiKey}/document")]
+        public async Task<ResultValueDto<DocumentRDto>> CreateWorkspaceDocument(string ticket, string wrkspApiKey,[FromHeader] string apiKeyPassword,  [FromHeader] string context, [FromBody] DocumentCDto document)
+        {
+            CheckAccess(ticket);
+            return await Call(async () =>
+            {
+                var id = await _srvWorkspace.GetWorkspaceIdByAPIAccess(wrkspApiKey, apiKeyPassword, context);
+                return await _srvDocument.CreateDocumentAsync(id, document);
+            });
+        }
+
+        [HttpPost("{Ticket}/workspace/{wrkspApiKey}/document/{documentId}/artifact/{artifactId}/content/input")]
+        public async Task<ResultValueDto<ArtifactRDto>> AppendWorkspaceDocumentArtifact(string ticket, string wrkspApiKey, long documentId, long artifactId,[FromHeader] string apiKeyPassword, [FromHeader] string context, [FromBody] ArtifactCDto artifact, IFormFile content)
+        {
+            CheckAccess(ticket);
+            return await Call(async () =>
+            {
+                var id = await _srvWorkspace.GetWorkspaceIdByAPIAccess(wrkspApiKey, apiKeyPassword, context);
+                await using var stream = content.OpenReadStream();
+                return await _srvDocument.AppendArtifactAsync(id, documentId, artifact, ArtifactTypeEnum.Input, stream);
+            });
+        }
+
+
+        #endregion
     }
 }

+ 3 - 0
AppServer/Web/Startup.cs

@@ -59,6 +59,7 @@ namespace AppServer
             services.AddScoped<StatisticsService>();
             services.AddScoped<CatalogueService>();
             services.AddScoped<WorkspaceService>();
+            services.AddScoped<StorageService>();
 
             // Bo Repos
             services.AddScoped<DataDomain, BOContext>();
@@ -70,6 +71,8 @@ namespace AppServer
             services.AddScoped<MimeTypeRepo>();
             services.AddScoped<StatisticRepo>();
             services.AddScoped<WorkspaceRepo>();
+            services.AddScoped<MetadocumentRepo>();
+            services.AddScoped<ArtifactRepo>();
 
 
             // Session framework

+ 5 - 0
AppServer/Web/appsettings.Development.json

@@ -29,6 +29,11 @@
         "Password": "Pan1d0mAc1",
         "Email": "admin@bo.com",
         "Culture": "en-US"
+      },
+      "Storage": {
+        "InputPath": "C:\\BOFiles\\Input",
+        "OutputPath": "C:\\BOFiles\\Output",
+        "IoBufferSize": 4096
       }
     },
     "Business": {

+ 5 - 0
AppServer/Web/appsettings.json

@@ -29,6 +29,11 @@
         "Password": "Pan1d0mAc1",
         "Email": "admin@bo.com",
         "Culture": "en-US"
+      },
+      "Storage": {
+        "InputPath": "C:\\BOFiles\\Input",
+        "OutputPath": "C:\\BOFiles\\Output",
+        "IoBufferSize": 4096
       }
     },
     "Business": {

+ 44 - 0
Console/Commands/Document/DocumentAddCommand.cs

@@ -0,0 +1,44 @@
+using BO.AppServer.Metadata.Dto;
+using BO.Console.Commands.Base;
+using Quadarax.Foundation.Core.QConsole;
+using Quadarax.Foundation.Core.QConsole.Attributes;
+
+namespace BO.Console.Commands.Workspace
+{
+    [CommandDefinition]
+    internal class WorkspaceAddCommand : AbstractPostCommand<WorkspaceCDto, WorkspaceRDto>
+    {
+        #region *** Constants ***
+        private const string CS_CMD_WKSPADD_NAME = "workspace_add";
+        private const string CS_CMD_WKSPADD_DESCR = "Add new workspace/s assigned to user by import file.";
+
+        #endregion
+
+        #region *** Properties ***
+
+        public override string Name => CS_CMD_WKSPADD_NAME;
+        public override string Description => CS_CMD_WKSPADD_DESCR;
+
+        #endregion
+
+        #region *** Constructor ***
+        public WorkspaceAddCommand(Engine engine) : base(engine)
+        {
+        }
+        #endregion
+
+        #region *** EXECUTE ***
+        protected override  WorkspaceRDto IterationExecute(WorkspaceCDto data)
+        {
+            WriteDebugInfo($"Workspace '{data.Name}' for user '{data.AssignedUserName}' starts creating...");
+            var result = Client.CreateWorkspaceAsync(data).Result;
+            WriteCaption($"Workspace '{data.Name}' for user '{data.AssignedUserName}' created.");
+            return result;
+        }
+
+        #endregion
+
+        #region *** Private operations ***
+        #endregion
+    }
+}

+ 78 - 0
Console/Commands/Document/DocumentsGetCommand.cs

@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.Linq;
+using BO.AppServer.Metadata.Enums;
+using BO.Console.Commands.Base;
+using Microsoft.VisualBasic;
+using Quadarax.Foundation.Core.QConsole;
+using Quadarax.Foundation.Core.QConsole.Argument;
+using Quadarax.Foundation.Core.QConsole.Attributes;
+using Quadarax.Foundation.Core.QConsole.Value;
+using Quadarax.Foundation.Core.Value;
+
+namespace BO.Console.Commands.Workspace
+{
+    [CommandDefinition]
+    internal class WorkspacesGetCommand : AbstractListScopedCommand<AppServer.Metadata.Enums.WorkspaceScopeEnums>
+    {
+        #region *** Constants ***
+
+        private const string CS_CMD_WRKSPGET_NAME = "workspace_list";
+        private const string CS_CMD_WRKSPGET_DESCR = "Get list of workspaces by specification scope.";
+
+
+        private const string CS_ARG_NAME_USER = "user";
+        private const string CS_ARG_HINT_USER = "<user_name>";
+        private const string CS_ARG_DESC_USER = "User name context when scope 'My' is selected. If not specified, use current user context.";
+
+        #endregion
+
+        #region *** Properties ***
+
+        public override string Name => CS_CMD_WRKSPGET_NAME;
+        public override string Description => CS_CMD_WRKSPGET_DESCR;
+
+        #endregion
+
+        #region *** Constructor ***
+
+        public WorkspacesGetCommand(Engine engine) : base(engine)
+        {
+        }
+
+        #endregion
+
+        #region *** EXECUTE ***
+
+        protected override Result OnExecute()
+        {
+            var workspaces = Client.GetWorkspacesAsync(Scope,GetArgumentValueOrDefault<string>(CS_ARG_NAME_USER), Paging).Result;
+            WriteDtoToConsole(workspaces,"Id","Name","Created","DocumentsCount", "ApiKey", "ApiPassword");
+            return new Result();
+        }
+
+        #endregion
+
+        #region *** Private operations ***
+
+        protected override void OnValidateArguments()
+        {
+            base.OnValidateArguments();
+            if (Scope != WorkspaceScopeEnums.My && WasArgumentSpecified(CS_ARG_NAME_USER))
+            {
+                WriteWarning($"Argument {Engine.Configuration.CharacterNamedArgumentSeparator}{CS_ARG_NAME_USER} is not allowed in scope '{Scope}'. Skipped.");
+                var arg = GetArgument(CS_ARG_NAME_USER);
+                arg.SetValueAsDefault();
+            }
+
+        }
+
+        protected override IEnumerable<AbstractArgument> OnSetupArguments()
+        {
+            var args = base.OnSetupArguments().ToList();
+            args.Add(new NamedArgument(CS_ARG_NAME_USER, CS_ARG_DESC_USER, CS_ARG_HINT_USER, TypeValuesEnum.String, string.Empty, false));
+            return args;
+        }
+
+        #endregion
+    }
+}