Browse Source

Add WorkspaceSyncCommand, move connection strings to configuration

Dalibor Votruba 4 years ago
parent
commit
ea7873f15e
36 changed files with 774 additions and 72 deletions
  1. 14 0
      @Database/bo_clear_data.sql
  2. 1 1
      AppServer/Business/Mapper/MapperLong.cs
  3. 32 0
      AppServer/Business/Security/Extensions/UserManagerExt.cs
  4. 46 0
      AppServer/Business/Security/RoleUtils.cs
  5. 3 2
      AppServer/Business/Services/AccessService.cs
  6. 16 3
      AppServer/Business/Services/Base/UserContextService.cs
  7. 2 1
      AppServer/Business/Services/CatalogueService.cs
  8. 31 11
      AppServer/Business/Services/ConfigurationService.cs
  9. 2 1
      AppServer/Business/Services/DocumentService.cs
  10. 3 2
      AppServer/Business/Services/StorageService.cs
  11. 4 2
      AppServer/Business/Services/UserService.cs
  12. 34 6
      AppServer/Business/Services/WorkspaceService.cs
  13. 16 2
      AppServer/Connector.Console/Connection.cs
  14. 1 0
      AppServer/Data/Data.csproj
  15. 1 16
      AppServer/Data/Entity/BOContext.cs
  16. 31 1
      AppServer/Data/Entity/BOContext.decl.cs
  17. 7 1
      AppServer/Data/Repository/WorkspaceRepo.cs
  18. 6 1
      AppServer/Metadata/Configuration/Configuration.cs
  19. 9 1
      AppServer/Metadata/Enums/IdentificationTypeEnum.cs
  20. 19 4
      AppServer/Web/Data/IFDbContext.cs
  21. 1 1
      AppServer/Web/Properties/launchSettings.json
  22. 1 1
      AppServer/Web/Services/ConsoleController.cs
  23. 3 3
      AppServer/Web/Startup.cs
  24. 5 2
      AppServer/Web/appsettings.Development.json
  25. 3 0
      AppServer/Web/appsettings.json
  26. 27 0
      Common/qdr.fnd.core.data/Extensions/ConnectionStringHelper.cs
  27. 23 0
      Common/qdr.fnd.core.data/Mapper/Mapper.cs
  28. 8 1
      Common/qdr.fnd.core/Data/Diff/DiffEntry.cs
  29. 19 0
      Common/qdr.fnd.core/IO/FileUtils.cs
  30. 0 3
      Console/Commands/Document/DocumentGetCommand.cs
  31. 24 0
      Console/Commands/Workspace/Sync/Diff/DocumentEntry.cs
  32. 17 0
      Console/Commands/Workspace/Sync/Diff/DocumentItem.cs
  33. 100 0
      Console/Commands/Workspace/Sync/DocumentEntryComparer.cs
  34. 259 0
      Console/Commands/Workspace/WorkspaceSyncCommand.cs
  35. 1 1
      Console/Properties/launchSettings.json
  36. 5 5
      Console/console.json

+ 14 - 0
@Database/bo_clear_data.sql

@@ -0,0 +1,14 @@
+USE [BO]
+GO
+
+EXEC sp_msforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"
+GO
+
+EXEC sp_msforeachtable "DELETE FROM ?"
+GO
+
+EXEC sp_msforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all"
+GO
+
+EXEC sp_msforeachtable "DBCC CHECKIDENT('?', RESEED, 1)"
+GO

+ 1 - 1
AppServer/Business/Mapper/MapperLong.cs

@@ -45,8 +45,8 @@ namespace BO.AppServer.Business.Mapper
             Define<WorkspaceCDto, Workspace>();
             Define<WorkspaceUDto, Workspace>();
 
+            DefineReverse<DocumentCDto, Metadocument>();
 
-            Define<DocumentCDto, Metadocument>();
             Define<Metadocument, DocumentRDto>(builder =>
             {
                 builder.Custom(dest => dest.Audit, src => new BoAuditDto()

+ 32 - 0
AppServer/Business/Security/Extensions/UserManagerExt.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+
+namespace BO.AppServer.Business.Security.Extensions
+{
+    internal static class UserManagerExt
+    {
+        public static async Task<bool> HasUserRoleAny(this UserManager<IdentityUser> userMan, string userName, IEnumerable<string> roles)
+        {
+            if (userMan == null)
+                throw new ArgumentNullException(nameof(userMan));
+
+            if (string.IsNullOrEmpty(userName))
+                throw new ArgumentNullException(nameof(userName));
+
+            if (roles==null)
+                throw new ArgumentNullException(nameof(roles));
+
+            var user = await userMan.FindByNameAsync(userName);
+            if (user == null)
+                throw new ArgumentOutOfRangeException(nameof(user), $"User '{userName}' not found.");
+
+            var ownedRoles = await userMan.GetRolesAsync(user);
+
+            return roles.Any(x => ownedRoles.Contains(x));
+
+        }
+    }
+}

+ 46 - 0
AppServer/Business/Security/RoleUtils.cs

@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using BO.AppServer.Metadata.Enums;
+
+namespace BO.AppServer.Business.Security
+{
+    internal class RoleUtils
+    {
+        public static IEnumerable<string> GetElevatedRoles(bool isNormalized = false)
+        {
+            return GetRoles(isNormalized, RoleEnum.System, RoleEnum.Console, RoleEnum.Service);
+        }
+
+        public static IEnumerable<string> GetUserRoles(bool isNormalized = false)
+        {
+            return GetRoles(isNormalized, RoleEnum.User, RoleEnum.Uploader, RoleEnum.Downloader, RoleEnum.Accountant, RoleEnum.Admin);
+        }
+
+        public static IEnumerable<string> GetAdminRoles(bool isNormalized = false)
+        {
+            return GetRoles(isNormalized, RoleEnum.SuperAdmin,RoleEnum.System);
+        }
+
+        public static IEnumerable<string> GetSystemRoles(bool isNormalized = false)
+        {
+            return GetRoles(isNormalized, RoleEnum.System, RoleEnum.Console, RoleEnum.B2B, RoleEnum.Service);
+        }
+
+        public static IEnumerable<string> GetAllRoles(bool isNormalized = false)
+        {
+            var roles = Enum.GetNames(typeof(RoleEnum));
+            return isNormalized ? roles.Select(x => x.ToUpper()) : roles;
+        }
+
+        private static IEnumerable<string> GetRoles(bool isNormalized, params RoleEnum[] roles)
+        {
+            if (roles == null)
+                throw new ArgumentNullException(nameof(roles));
+            if (roles.Length == 0)
+                return Array.Empty<string>();
+
+            return isNormalized ? roles.Select(x => x.ToString().ToUpper()) : roles.Select(x => x.ToString());
+        }
+    }
+}

+ 3 - 2
AppServer/Business/Services/AccessService.cs

@@ -3,6 +3,7 @@ using System.Security.Principal;
 using System.Threading.Tasks;
 using BO.AppServer.Business.Mapper;
 using BO.AppServer.Data.Entity;
+using BO.AppServer.Metadata.Configuration;
 using BO.AppServer.Metadata.Dto;
 using BO.AppServer.Metadata.Enums;
 using Microsoft.AspNetCore.Http;
@@ -24,8 +25,8 @@ namespace BO.AppServer.Business.Services
         private readonly UserManager<IdentityUser> _manUsers;
         private readonly UserRepo _repoUsers;
         private readonly IHttpContextAccessor _context;
-        private readonly Configuration.Configuration _configuration;
-        public AccessService(IOptions<Configuration.Configuration> config,
+        private readonly Configuration _configuration;
+        public AccessService(IOptions<Configuration> config,
             SignInManager<IdentityUser> manSignIn,
             UserManager<IdentityUser> manUsers,
             UserRepo repoUsers,

+ 16 - 3
AppServer/Business/Services/Base/UserContextService.cs

@@ -1,7 +1,12 @@
 using System;
+using System.Linq;
 using System.Security.Principal;
+using System.Threading.Tasks;
 using BO.AppServer.Business.Exceptions;
+using BO.AppServer.Business.Security;
+using BO.AppServer.Business.Security.Extensions;
 using BO.AppServer.Data.Entity;
+using BO.AppServer.Metadata.Configuration;
 using BO.AppServer.Metadata.Enums;
 using Microsoft.AspNetCore.Identity;
 using Microsoft.Extensions.Logging;
@@ -15,9 +20,9 @@ namespace BO.AppServer.Business.Services.Base
     {
         protected UserManager<IdentityUser> _manUser;
         protected UserRepo _repoUser;
-        protected Configuration.Configuration _configuration;
+        protected Configuration _configuration;
 
-        protected UserContextService(IOptions<Configuration.Configuration> config, UserRepo repoUser, UserManager<IdentityUser> userManager, IPrincipal currentPrincipal, ILoggerFactory logger) : base(MaySetDefaultPrincipal(currentPrincipal, config), logger)
+        protected UserContextService(IOptions<Configuration> config, UserRepo repoUser, UserManager<IdentityUser> userManager, IPrincipal currentPrincipal, ILoggerFactory logger) : base(MaySetDefaultPrincipal(currentPrincipal, config), logger)
         {
             _manUser = userManager ?? throw new ArgumentNullException(nameof(userManager));
             _repoUser = repoUser ?? throw new ArgumentNullException(nameof(repoUser));
@@ -36,7 +41,15 @@ namespace BO.AppServer.Business.Services.Base
             return user.Id;
         }
 
-        private static IPrincipal MaySetDefaultPrincipal(IPrincipal principal, IOptions<Configuration.Configuration> config)
+        protected async Task<bool> HasUserElevation(string alternativeCurrentUserName)
+        {
+            var owner = string.IsNullOrEmpty(alternativeCurrentUserName) ? CurrentPrincipal.Identity.Name : alternativeCurrentUserName;
+            var result = await _manUser.HasUserRoleAny(owner, RoleUtils.GetElevatedRoles());
+            Log.LogTrace($"User '{owner}' {(result ? "has" : "hasn't")} elevation.");
+            return result;
+        }
+
+        private static IPrincipal MaySetDefaultPrincipal(IPrincipal principal, IOptions<Configuration> config)
         {
             if (principal?.Identity == null || principal.Identity.Name == "genericIdentity")
                 principal = new GenericPrincipal(new GenericIdentity(config.Value.System.AccountSystem.Name),new[] { RoleEnum.System.ToString() });

+ 2 - 1
AppServer/Business/Services/CatalogueService.cs

@@ -7,6 +7,7 @@ using BO.AppServer.Business.Exceptions;
 using BO.AppServer.Business.Mapper;
 using BO.AppServer.Business.Services.Base;
 using BO.AppServer.Data.Entity;
+using BO.AppServer.Metadata.Configuration;
 using BO.AppServer.Metadata.Dto;
 using BO.AppServer.Metadata.Enums;
 using Microsoft.AspNetCore.Identity;
@@ -35,7 +36,7 @@ namespace BO.AppServer.Business.Services
             BillingPlanRepo repoBillingPlan,
             UserRepo repoUser, 
             UserManager<IdentityUser> userManager, 
-            IOptions<Configuration.Configuration> config,
+            IOptions<Configuration> config,
             IPrincipal currentPrincipal, ILoggerFactory logger) : base(config, repoUser, userManager, currentPrincipal, logger)
         {
             _repoMimeType = repoMimeType ?? throw new ArgumentNullException(nameof(repoMimeType));

+ 31 - 11
AppServer/Business/Services/ConfigurationService.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Security.Principal;
 using System.Threading.Tasks;
+using BO.AppServer.Metadata.Configuration;
 using BO.AppServer.Metadata.Dto;
 using BO.AppServer.Metadata.Enums;
 using Microsoft.Extensions.DependencyInjection;
@@ -12,13 +13,25 @@ using Quadarax.Foundation.Core.Data.Interface.Entity.Dto;
 
 namespace BO.AppServer.Business.Services
 {
+
+
     public class ConfigurationService : AbstractService
     {
+        #region *** Constants ***
+        private const string CS_WRK_DEV_NAME = "TestWorkspace";
+        private const string CS_WRK_DEV_APIPWD = "test";
+        private const string CS_USR_DEV_NAME = "test";
+        private const string CS_USR_DEV_PWD = "password";
+        private const string CS_USR_DEV_EML = "test@boo.com";
+        private const string CS_BP_DEV_CODE = "UNLIMITED";
+        #endregion
+
+
         private IServiceProvider _serviceProvider;
 
-        public Configuration.Configuration Configuration { get; }
+        public Configuration Configuration { get; }
 
-        public ConfigurationService(IOptions<Configuration.Configuration> config,
+        public ConfigurationService(IOptions<Configuration> config,
             IServiceProvider serviceProvider,
             IPrincipal currentPrincipal, 
             ILoggerFactory logger) : base(currentPrincipal, logger)
@@ -83,7 +96,7 @@ namespace BO.AppServer.Business.Services
                 var task0 = srvCatalogues.CreateBillingPlanAsync(new BillingPlanCDto()
                 {
                     Name = "Unlimited",
-                    Code = "UNLIMITED",
+                    Code = CS_BP_DEV_CODE,
                     Description = "This plan is only for testing purpose",
                     PstinitialCredits = 1000,
                     PstcreditsPerItem = 1,
@@ -115,17 +128,24 @@ namespace BO.AppServer.Business.Services
                 }, true);
 
 
-                var task3 = EnsureUser(srvUsers, "test", "password", "test@bo.com", 1029,
+                var task3 = EnsureUser(srvUsers, CS_USR_DEV_NAME, CS_USR_DEV_PWD, CS_USR_DEV_EML, 1029,
                     new[] { RoleEnum.User, RoleEnum.Admin });
 
                 Task.WhenAll(task0, task1, task2, task3)
-                    .ContinueWith(async (t) => await srvWorkspaces.CreateWorkspaceAsync(new WorkspaceCDto()
-                {
-                    Name = "TestWorkspace",
-                    Apipassword = "test",
-                    BillingPlanCode = "UNLIMITED",
-                    AssignedUserName = "test"
-                })).Wait();
+                    .ContinueWith(async (t) =>
+                    {
+
+                        if (await srvWorkspaces.ExistsWorkspace(WorkspaceIdentificationTypeEnum.Name, CS_WRK_DEV_NAME))
+                            return await srvWorkspaces.GetWorkspace(WorkspaceIdentificationTypeEnum.Name, CS_WRK_DEV_NAME);
+
+                        return await srvWorkspaces.CreateWorkspaceAsync(new WorkspaceCDto()
+                        {
+                            Name = CS_WRK_DEV_NAME,
+                            Apipassword = CS_WRK_DEV_APIPWD,
+                            BillingPlanCode = CS_BP_DEV_CODE,
+                            AssignedUserName = CS_USR_DEV_NAME
+                        });
+                    }).Wait();
             }
             return new ResultPlain();
         }

+ 2 - 1
AppServer/Business/Services/DocumentService.cs

@@ -10,6 +10,7 @@ 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.Configuration;
 using BO.AppServer.Metadata.Dto;
 using BO.AppServer.Metadata.Enums;
 using Microsoft.AspNetCore.Identity;
@@ -42,7 +43,7 @@ namespace BO.AppServer.Business.Services
             ArtifactRepo repoArtifact,
             StorageService srvStorage,
             IFileSystem fileSystem,
-            IOptions<Configuration.Configuration> config,
+            IOptions<Configuration> config,
             UserRepo repoUser, 
             UserManager<IdentityUser> userManager, 
             IPrincipal currentPrincipal, 

+ 3 - 2
AppServer/Business/Services/StorageService.cs

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
 using BO.AppServer.Business.Exceptions;
 using BO.AppServer.Data.Entity;
 using BO.AppServer.Data.Repository;
+using BO.AppServer.Metadata.Configuration;
 using BO.AppServer.Metadata.Enums;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
@@ -20,12 +21,12 @@ namespace BO.AppServer.Business.Services
     {
         #region *** Private fields ***
         private IFileSystem _fileSystem;
-        private IOptions<Configuration.Configuration> _config;
+        private IOptions<Configuration> _config;
         private ArtifactRepo _repoArtifact;
         #endregion
         #region *** Constructors ***
         public StorageService(ArtifactRepo repoArtifact,
-            IOptions<Configuration.Configuration> config, 
+            IOptions<Configuration> config, 
             IFileSystem fileSystem, 
             IPrincipal currentPrincipal, 
             ILoggerFactory logger) : base(currentPrincipal, logger)

+ 4 - 2
AppServer/Business/Services/UserService.cs

@@ -5,8 +5,10 @@ using System.Linq;
 using System.Security.Principal;
 using System.Threading.Tasks;
 using BO.AppServer.Business.Mapper;
+using BO.AppServer.Business.Security;
 using BO.AppServer.Business.Services.Base;
 using BO.AppServer.Data.Entity;
+using BO.AppServer.Metadata.Configuration;
 using BO.AppServer.Metadata.Dto;
 using BO.AppServer.Metadata.Enums;
 using Microsoft.AspNetCore.Identity;
@@ -34,7 +36,7 @@ namespace BO.AppServer.Business.Services
             WorkspaceRepo repoWorkspace,
             AccessService srvAccessService,
             RoleManager<IdentityRole> roleManager, 
-            IOptions<Configuration.Configuration> config,
+            IOptions<Configuration> config,
             UserRepo repoUser,
             UserManager<IdentityUser> userManager,
             IPrincipal currentPrincipal,
@@ -194,7 +196,7 @@ namespace BO.AppServer.Business.Services
 
         public async Task EnsureRolesAsync()
         {
-            var roles = Enum.GetNames(typeof(RoleEnum));
+            var roles = RoleUtils.GetAllRoles();
             foreach (var roleName in roles)
             {
                 if (!await _manRole.RoleExistsAsync(roleName))

+ 34 - 6
AppServer/Business/Services/WorkspaceService.cs

@@ -9,6 +9,7 @@ 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.Configuration;
 using BO.AppServer.Metadata.Dto;
 using BO.AppServer.Metadata.Enums;
 using Microsoft.AspNetCore.Identity;
@@ -33,7 +34,7 @@ namespace BO.AppServer.Business.Services
         public WorkspaceService(WorkspaceRepo repoWorkspace, 
             BillingPlanRepo repoBillingPlan,
             WorkspaceBillingRepo repoWorkspaceBilling,
-            IOptions<Configuration.Configuration> config,
+            IOptions<Configuration> config,
             UserRepo repoUser, 
             UserManager<IdentityUser> userManager, 
             IPrincipal currentPrincipal, 
@@ -66,6 +67,7 @@ namespace BO.AppServer.Business.Services
                 throw exc;
             }
 
+
             var current = GetCurrentUser();
             var newWorkspace = _repoWorkspace.CreateWorkspace(workspace.Name, billingPlan, workspace.Apipassword, true, owner,current);
             _repoWorkspace.Commit();
@@ -114,8 +116,21 @@ namespace BO.AppServer.Business.Services
             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);
+            Workspace workspace = null;
+            if (await HasUserElevation(currentUserName))
+            {
+                // if elevated user and system user
+                workspace = _repoWorkspace.GetByApiKey(apiKey);
+                if (workspace.Apipassword != apiPassword)
+                    workspace = null;
+            }
+            else
+            {
+                // for rest
+                workspace = _repoWorkspace.GetAllByOwner(owner, new Paging())
+                    .FirstOrDefault(x => x.Apikey == apiKeyGuid && x.Apipassword == apiPassword);
+            }
+
 
             if (workspace == null)
             {
@@ -127,11 +142,12 @@ namespace BO.AppServer.Business.Services
             return workspace.Id;
         }
 
-        public async Task<ResultValueDto<WorkspaceRDto>> GetWorkspace(IdentificationTypeEnum identificationType, string identificationValue)
+        public async Task<ResultValueDto<WorkspaceRDto>> GetWorkspace(WorkspaceIdentificationTypeEnum identificationType, string identificationValue)
         {
-            var workspace = identificationType == IdentificationTypeEnum.Id ? 
+            var workspace = identificationType == WorkspaceIdentificationTypeEnum.Id ? 
                 _repoWorkspace.Get(ArgumentAsLong(nameof(identificationValue), identificationValue)) 
-                : _repoWorkspace.GetByName(identificationValue);
+                : identificationType == WorkspaceIdentificationTypeEnum.ApiKey ? _repoWorkspace.GetByApiKey(identificationValue) 
+                    : _repoWorkspace.GetByName(identificationValue);
 
             
             if (workspace == null)
@@ -146,6 +162,18 @@ namespace BO.AppServer.Business.Services
             return result;
         }
 
+        public async Task<bool> ExistsWorkspace(WorkspaceIdentificationTypeEnum identificationType, string identificationValue)
+        {
+            var workspace = identificationType == WorkspaceIdentificationTypeEnum.Id ? 
+                _repoWorkspace.Get(ArgumentAsLong(nameof(identificationValue), identificationValue)) 
+                : identificationType == WorkspaceIdentificationTypeEnum.ApiKey ? _repoWorkspace.GetByApiKey(identificationValue) 
+                    : _repoWorkspace.GetByName(identificationValue);
+
+
+            return workspace != null;
+        }
+
+
         #endregion
 
 

+ 16 - 2
AppServer/Connector.Console/Connection.cs

@@ -200,13 +200,27 @@ namespace BO.Connector.Console
             return result.Value;
         }
 
-        public async Task<WorkspaceRDto> GetWorkspaceAsync(IdentificationTypeEnum identificationType, string identificationValue)
+        public async Task<WorkspaceRDto> GetWorkspaceAsync(WorkspaceIdentificationTypeEnum identificationType, string identificationValue)
         {
             CheckIsOpen();
             var result = await CallRequestAsync<ResultValueDto<WorkspaceRDto>>($"{Ticket}/workspace/{identificationType}/{identificationValue}", RestCallTypeEnum.Get);
             return result.Value;
         }
-
+        public async Task<WorkspaceRDto> GetWorkspaceAsync(IdentificationTypeEnum identificationType, string identificationValue)
+        {
+            var idType = WorkspaceIdentificationTypeEnum.Id;
+            switch (identificationType)
+            {
+                case IdentificationTypeEnum.Id:
+                    break;
+                case IdentificationTypeEnum.Name:
+                    idType = WorkspaceIdentificationTypeEnum.Name;
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(identificationType));
+            }
+            return await GetWorkspaceAsync(idType , identificationValue);
+        }
 
         public async Task<IEnumerable<WorkspaceRDto>> GetWorkspacesAsync(WorkspaceScopeEnums scope,string userNameContext, PagingDto paging = null)
         {

+ 1 - 0
AppServer/Data/Data.csproj

@@ -20,6 +20,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\..\Common\qdr.fnd.core.data\qdr.fnd.core.data.csproj" />
+    <ProjectReference Include="..\Metadata\Metadata.csproj" />
   </ItemGroup>
 
   <ItemGroup>

+ 1 - 16
AppServer/Data/Entity/BOContext.cs

@@ -1,5 +1,4 @@
-using System;
-using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
 using Quadarax.Foundation.Core.Data.Domain;
 
 #nullable disable
@@ -9,11 +8,6 @@ namespace BO.AppServer.Data.Entity
     public partial class BOContext : DataDomain
     {
 
-        public BOContext(DbContextOptions<BOContext> options)
-            : base(options)
-        {
-        }
-
         public virtual DbSet<Artifact> Artifacts { get; set; }
         public virtual DbSet<BillingInfo> BillingInfos { get; set; }
         public virtual DbSet<BillingPlan> BillingPlans { get; set; }
@@ -34,15 +28,6 @@ namespace BO.AppServer.Data.Entity
         public virtual DbSet<Workspace> Workspaces { get; set; }
         public virtual DbSet<WorkspaceBilling> WorkspaceBillings { get; set; }
 
-        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
-        {
-            if (!optionsBuilder.IsConfigured)
-            {
-#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
-                optionsBuilder.UseSqlServer("Server=(local);Database=BO;Trusted_Connection=True;");
-            }
-        }
-
         protected override void OnModelCreating(ModelBuilder modelBuilder)
         {
             modelBuilder.HasAnnotation("Relational:Collation", "Czech_CI_AS");

+ 31 - 1
AppServer/Data/Entity/BOContext.decl.cs

@@ -1,9 +1,39 @@
-using Microsoft.EntityFrameworkCore;
+using System;
+using BO.AppServer.Metadata.Configuration;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Quadarax.Foundation.Core.Data.Extensions;
 
 namespace BO.AppServer.Data.Entity
 {
     public partial class BOContext
     {
+        private readonly string _connectionString;
+        private readonly ILogger _logger;
+
+        public BOContext(DbContextOptions<BOContext> options, IOptions<Configuration> config, ILoggerFactory logger)
+            : base(options)
+        {
+            if (config == null)
+                throw new ArgumentNullException(nameof(config));
+
+            _connectionString = config.Value.System.DbConnectionBO;
+            if (logger == null)
+                throw new ArgumentNullException(nameof(logger));
+            _logger = logger.CreateLogger<BOContext>();
+        }
+
+        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+        {
+            if (!optionsBuilder.IsConfigured)
+            {
+                var cs = new ConnectionStringHelper(_connectionString);
+                _logger.Log(LogLevel.Information, $"Connecting to database '{cs}' ...");
+                optionsBuilder.UseSqlServer(_connectionString);
+            }
+        }
+
         partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
         {
             modelBuilder.Entity<Workspace>(entity =>

+ 7 - 1
AppServer/Data/Repository/WorkspaceRepo.cs

@@ -64,10 +64,16 @@ public class WorkspaceRepo : EntityRepository<long,  Workspace>
             return Query(new Paging()).FirstOrDefault(x => x.Name.ToLower() == name.ToLower());
         }
 
+        public Workspace GetByApiKey(string apiKey)
+        {
+            return Query(new Paging()).FirstOrDefault(x => x.Apikey == Guid.Parse(apiKey));
+        }
+
+
         public IQueryable<Workspace> GetAllByOwner(User owner, IPaging paging)
         {
             var userWorkspaceSet = Context.GetDbSet<DbSet<UserWorkspace>>();
             return userWorkspaceSet.Page(paging.IsDisabled ? (int?)null : paging.Page, paging.PageSize)
-                .Where(x => x.User == owner).Select(x => x.Workspace);
+                .Where(x => x.User == owner || owner==null).Select(x => x.Workspace);
         }
     }

+ 6 - 1
AppServer/Business/Configuration/Configuration.cs → AppServer/Metadata/Configuration/Configuration.cs

@@ -1,7 +1,7 @@
 using System.Collections.Generic;
 using System.Globalization;
 
-namespace BO.AppServer.Business.Configuration
+namespace BO.AppServer.Metadata.Configuration
 {
     public class Configuration
     {
@@ -21,11 +21,16 @@ namespace BO.AppServer.Business.Configuration
 
         public class CfgSystem
         {
+            public string DbConnectionBO { get; set; }
+            public string DbConnectionIF { get; set; }
+
             public CfgSystemAccount AccountSystem { get; set; }
             public CfgSystemAccount AccountConsole { get; set; }
             public CfgSystemAccount AccountAdmin { get; set; }
             public string SessionIdleTimeout { get; set; }
             public CfgSystemStorage Storage { get; set; }
+            public bool UseSSL { get; set; }
+
         }
 
         public class CfgSystemAccount

+ 9 - 1
AppServer/Metadata/Enums/IdentificationTypeEnum.cs

@@ -3,6 +3,14 @@
     public enum IdentificationTypeEnum
     {
         Id,
-        Name
+        Name,
+    }
+
+
+    public enum WorkspaceIdentificationTypeEnum
+    {
+        Id,
+        Name,
+        ApiKey
     }
 }

+ 19 - 4
AppServer/Web/Data/IFDbContext.cs

@@ -1,21 +1,36 @@
-using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
+using System;
+using BO.AppServer.Metadata.Configuration;
+using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Quadarax.Foundation.Core.Data.Extensions;
 
 namespace BO.AppServer.Web.Data
 {
     public class IFDbContext : IdentityDbContext
     {
-        public IFDbContext(DbContextOptions options)
+        private readonly string _connectionString;
+        private readonly ILogger _logger;
+
+        public IFDbContext(DbContextOptions options, IOptions<Configuration> config, ILoggerFactory logger)
             : base(options)
         {
+            if (config == null)
+                throw new ArgumentNullException(nameof(config));
+            _connectionString = config.Value.System.DbConnectionIF;
+            if (logger == null)
+                throw new ArgumentNullException(nameof(logger));
+            _logger = logger.CreateLogger<IFDbContext>();
         }
 
         protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
         {
             if (!optionsBuilder.IsConfigured)
             {
-#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
-                optionsBuilder.UseSqlServer("Server=(local);Database=BO_IF;Trusted_Connection=True;");
+                var cs = new ConnectionStringHelper(_connectionString);
+                _logger.Log(LogLevel.Information, $"Connecting to database '{cs}' ...");
+                optionsBuilder.UseSqlServer(_connectionString);
             }
         }
     }

+ 1 - 1
AppServer/Web/Properties/launchSettings.json

@@ -23,7 +23,7 @@
         "ASPNETCORE_ENVIRONMENT": "Development",
         "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"
       },
-      "applicationUrl": "https://localhost:5001;http://localhost:5000"
+      "applicationUrl": "https://192.168.1.101:5001;http://192.168.1.101:5000"
     }
   }
 }

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

@@ -140,7 +140,7 @@ namespace BO.AppServer.Web.Services
         }
 
         [HttpGet("{ticket}/workspace/{identificationType}/{identificationValue}")]
-        public async Task<ResultValueDto<WorkspaceRDto>> GetWorkspace(string ticket, IdentificationTypeEnum identificationType, string identificationValue)
+        public async Task<ResultValueDto<WorkspaceRDto>> GetWorkspace(string ticket, WorkspaceIdentificationTypeEnum identificationType, string identificationValue)
         {
             CheckAccess(ticket);
             return await Call(async () => await _srvWorkspace.GetWorkspace(identificationType, identificationValue));

+ 3 - 3
AppServer/Web/Startup.cs

@@ -1,7 +1,6 @@
 using System;
 using System.IO.Abstractions;
 using System.Security.Principal;
-using BO.AppServer.Business.Configuration;
 using BO.AppServer.Business.Services;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Builder;
@@ -12,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using BO.AppServer.Data.Entity;
 using BO.AppServer.Data.Repository;
+using BO.AppServer.Metadata.Configuration;
 using BO.AppServer.Web.Data;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Identity;
@@ -127,8 +127,8 @@ namespace AppServer
             _srvConfiguration = (ConfigurationService)service.GetService(typeof(ConfigurationService));
             lifetime.ApplicationStarted.Register(OnApplicationStarted);
 
-
-            app.UseHttpsRedirection();
+            if (_srvConfiguration.Configuration.System.UseSSL)
+                app.UseHttpsRedirection();
             app.UseStaticFiles();
 
             app.UseRouting();

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

@@ -4,14 +4,17 @@
       "Default": "Information",
       "Microsoft": "Warning",
       // Enable components logging
-      "Microsoft.Hosting.Lifetime": "Information",
+      "Microsoft.Hosting.Lifetime": "Information"
       // Enable EF logging
-      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
+      // "Microsoft.EntityFrameworkCore.Database.Command": "Information"
     }
   },
   "Bo": {
     "System": {
+      "DbConnectionBO": "Server=(local);Database=BO;Trusted_Connection=True;",    // For localDB use Server=(localDB), for SqlServer use Server=(local)
+      "DbConnectionIF": "Server=(local);Database=BO_IF;Trusted_Connection=True;", // For localDB use Server=(localDB), for SqlServer use Server=(local)
       "SessionIdleTimeout": "00:05:00",
+      "UseSSL": false,
       "AccountSystem": {
         "Name": "System",
         "Password": "Fr1cEKd0NTD13",

+ 3 - 0
AppServer/Web/appsettings.json

@@ -11,7 +11,10 @@
   },
   "Bo": {
     "System": {
+      "DbConnectionBO": "Server=(local);Database=BO;Trusted_Connection=True;",    // For localDB use Server=(localDB), for SqlServer use Server=(local)
+      "DbConnectionIF": "Server=(local);Database=BO_IF;Trusted_Connection=True;", // For localDB use Server=(localDB), for SqlServer use Server=(local)
       "SessionIdleTimeout": "00:05:00",
+      "UseSSL": true,
       "AccountSystem": {
         "Name": "System",
         "Password": "Fr1cEKd0NTD13",

+ 27 - 0
Common/qdr.fnd.core.data/Extensions/ConnectionStringHelper.cs

@@ -0,0 +1,27 @@
+using System.Data.Common;
+using System.Text;
+
+namespace Quadarax.Foundation.Core.Data.Extensions
+{
+    public class ConnectionStringHelper
+    {
+        private DbConnectionStringBuilder _builder;
+
+        public ConnectionStringHelper(string connectionString)
+        {
+            _builder = new DbConnectionStringBuilder
+            {
+                ConnectionString = connectionString
+            };
+        }
+
+        public override string ToString()
+        {
+            var sb = new StringBuilder();
+            sb.Append(_builder["Database"]);
+            sb.Append("@");
+            sb.Append(_builder["Server"]);
+            return sb.ToString();
+        }
+    }
+}

+ 23 - 0
Common/qdr.fnd.core.data/Mapper/Mapper.cs

@@ -53,6 +53,29 @@ namespace Quadarax.Foundation.Core.Data.Mapper
 
         }
 
+        protected void DefineReverse<TDto, TDao>(Action<LoomBuilder<TDto, TDao>> customFromDao)
+            where TDao : class, IDao<long>, new()
+            where TDto : class, IDto, new()
+        {
+            // from DTO to DAO
+            var builder = Define<TDto, TDao>();
+            builder.PublicProperties();
+            customFromDao?.Invoke(builder);
+            builder.Compile();
+
+        }
+
+        protected void DefineReverse<TDto, TDao>()
+            where TDao : class, IDao<long>, new()
+            where TDto : class, IDto, new()
+        {
+            // from DTO to DAO
+            var builder = Define<TDto, TDao>();
+            builder.PublicProperties();
+            builder.Compile();
+
+        }
+
 
         #endregion
     }

+ 8 - 1
Common/qdr.fnd.core/Data/Diff/DiffEntry.cs

@@ -1,4 +1,6 @@
-namespace Quadarax.Foundation.Core.Data.Diff
+using System.Threading;
+
+namespace Quadarax.Foundation.Core.Data.Diff
 {
     public abstract class DiffEntry
     {
@@ -54,6 +56,11 @@
             return OnCompareEquality(other);
         }
 
+        public TItem GetItem<TItem>()
+        {
+            return (TItem)Item;
+        }
+
         /// <summary>
         /// 
         /// </summary>

+ 19 - 0
Common/qdr.fnd.core/IO/FileUtils.cs

@@ -2,6 +2,7 @@
 using System.IO;
 using System.IO.Abstractions;
 using System.Reflection;
+using System.Text.RegularExpressions;
 using Quadarax.Foundation.Core.Value.Extensions;
 
 
@@ -11,6 +12,24 @@ namespace Quadarax.Foundation.Core.IO
     {
         private const string CS_CFG_VAR = "LocalAppData";
 
+
+        private static Regex _removeInvalidChars;
+
+        public static string SanitizedFileName(IFileSystem fileSystemAbstraction, string fileName, string replacement = "_")
+        {
+            if (fileSystemAbstraction == null)
+                throw new ArgumentNullException(nameof(fileSystemAbstraction));
+            if (string.IsNullOrEmpty(fileName))
+                throw new ArgumentNullException(nameof(fileName));
+
+            _removeInvalidChars ??= new Regex($"[{Regex.Escape(new string(fileSystemAbstraction.Path.GetInvalidFileNameChars()))}]",
+                RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.CultureInvariant);
+
+            return _removeInvalidChars.Replace(fileName, replacement);
+        }
+
+
+
         public static void TestWriteTempFile(IFileSystem fileSystemAbstraction, string directory)
         {
 

+ 0 - 3
Console/Commands/Document/DocumentGetCommand.cs

@@ -1,9 +1,6 @@
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Linq;
-using System.Threading.Tasks;
-using BO.AppServer.Metadata.Enums;
 using BO.Console.Commands.Base;
 using BO.Console.Extensions;
 using Quadarax.Foundation.Core.Logging;

+ 24 - 0
Console/Commands/Workspace/Sync/Diff/DocumentEntry.cs

@@ -0,0 +1,24 @@
+using Quadarax.Foundation.Core.Data.Diff;
+
+namespace BO.Console.Commands.Workspace.Sync.Diff
+{
+    internal class DocumentEntry : DiffEntry
+    {
+
+        public DocumentEntry()
+        {
+        }
+
+
+        public override object GetKey()
+        {
+            var item = (DocumentItem)Item;
+            return item.ArtId.GetValueOrDefault();
+        }
+
+        protected override CompareStateEnum OnCompareEquality(DiffEntry other)
+        {
+            return CompareStateEnum.CurrentSmaller;
+        }
+    }
+}

+ 17 - 0
Console/Commands/Workspace/Sync/Diff/DocumentItem.cs

@@ -0,0 +1,17 @@
+namespace BO.Console.Commands.Workspace.Sync.Diff
+{
+    internal class DocumentItem
+    {
+        public bool IsLocal { get; set; }
+
+        public long? DocId { get; set; }
+        public string DocName { get; set; }
+
+        public long? ArtId { get; set; }
+        public string ArtName { get; set; }
+        public string ArtExt { get; set; }
+        public long ArtLength { get; set; }
+
+        public string PhysicalPath { get; set; }
+    }
+}

+ 100 - 0
Console/Commands/Workspace/Sync/DocumentEntryComparer.cs

@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using BO.AppServer.Metadata.Dto;
+using BO.Console.Commands.Workspace.Sync.Diff;
+using Quadarax.Foundation.Core.Data.Diff;
+
+namespace BO.Console.Commands.Workspace.Sync
+{
+    internal class DocumentEntryComparer
+    {
+
+        #region *** Private fields ***
+        private IList<DocumentItem> _local = new List<DocumentItem>();
+        private IList<DocumentItem> _remote = new List<DocumentItem>();
+        #endregion
+
+
+        #region *** Public Operations ***
+        public void AddLocalDocument(string directoryName, string fileName, long fileSize)
+        {
+            var doc = ParseDocumentDirectory(directoryName);
+            if (doc != null)
+            {
+                var art = ParseArtifactFileName(fileName);
+                if (art != null)
+                {
+                    var item = new DocumentItem();
+                    item.ArtName = art.Item3;
+                    item.ArtExt = art.Item4;
+                    item.ArtId = art.Item1;
+                    item.ArtLength = fileSize;
+                    item.DocId = doc.Item1;
+                    item.DocName = doc.Item2;
+                    item.IsLocal = true;
+                    _remote.Add(item);
+                }
+            }
+        }
+
+        public void AddLocalDocumentNew(string fileName, bool bIn = true)
+        {
+        }
+
+        public void AddRemoteDocument(DocumentRDto document)
+        {
+            foreach (var art in document.Artifacts)
+            {
+                var item = new DocumentItem();
+                item.ArtName = art.Name;
+                item.ArtExt = art.Extension;
+                item.ArtId = art.Id;
+                item.ArtLength = art.Length;
+                item.DocId = document.Id;
+                item.DocName = document.Name;
+                item.IsLocal = false;
+                _remote.Add(item);
+            }
+        }
+
+        public DiffComparator<DocumentEntry> GetComparator()
+        {
+            return new DiffComparator<DocumentEntry>(_local, _remote);
+        }
+
+        public bool ValidateDocumentDirectory(string directoryName)
+        {
+            return ParseDocumentDirectory(directoryName)!=null;
+        }
+
+        public bool ValidateArtifactFileName(string artifactFileName)
+        {
+            return ParseArtifactFileName(artifactFileName) != null;
+        }
+
+        #endregion
+
+        //<documentId:8>.<document_name_normalized>
+        private Tuple<long, string> ParseDocumentDirectory(string directoryName)
+        {
+            var parts = directoryName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
+            if (parts.Length != 2)
+                return null;
+            if (!long.TryParse(parts[0], out var id))
+                return null;
+            return new Tuple<long, string>(id, parts[1]);
+        }
+
+        // <arifact_id>.<artifact_type>.<artifact_name>.<artifaxt_ext>
+        private Tuple<long, string, string, string> ParseArtifactFileName(string artifactFileName)
+        {
+            var parts = artifactFileName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
+            if (parts.Length != 4)
+                return null;
+            if (!long.TryParse(parts[0], out var id))
+                return null;
+            return new Tuple<long, string, string, string>(id, parts[1], parts[2], parts[3]);
+        }
+
+    }
+}

+ 259 - 0
Console/Commands/Workspace/WorkspaceSyncCommand.cs

@@ -0,0 +1,259 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.Design;
+using System.IO.Abstractions;
+using System.Linq;
+using BO.AppServer.Metadata.Dto;
+using BO.AppServer.Metadata.Enums;
+using BO.Console.Commands.Base;
+using BO.Console.Commands.Workspace.Sync;
+using BO.Console.Commands.Workspace.Sync.Diff;
+using Quadarax.Foundation.Core.Data.Diff;
+using Quadarax.Foundation.Core.IO;
+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 WorkspaceSyncCommand : AbstractWorkspaceCommand
+    {
+        #region *** Constants ***
+
+        private const string CS_NEW_IN = ".in";
+        private const string CS_NEW_OUT = ".out";
+
+        private const string CS_NOTES = "Directory structure:\n" + 
+                                        "["+CS_NEW_IN + "]\t- new artifacts directory for Input type\n" +
+                                        "\t\t<doc_name>.<artifact_name>.<artifact_ext> - new artifacts file\n" +
+                                        "\t\t...\n" +
+                                        "[" +CS_NEW_OUT + "]\t- new artifacts directory for Output type\n" +
+                                        "[<documentId:8>.<document_name_normalized>]\t- document directory\n" +
+                                        "\t\t<arifact_id>.<artifact_type>.<artifact_name>.<artifaxt_ext> - artifact file\n" +
+                                        "\t\t...\n" +
+                                        "...\n" +
+                                        "\n" +
+                                        "Process sequence:\n" +
+                                        "0. If init flag set, sync clean directory\n" +
+                                        "1. Get workspace documents with artifacts -> srv_arts\n" +
+                                        "2. Get sync directory documents with artifacts (skip not well formatted, process incomming also)  -> loac_arts\n" +
+                                        "3. Resolve differences srv_arts and loc_ars -> changed_arts\n" +
+                                        "4. Push changes changed_arts to database (if delete flag set removes documents in database)\n" +
+                                        "5. Pull changes changed_arts from database (if delete flag set removes documents in sync directory)\n" +
+                                        "6. Cleanup incommings\n" +
+                                        "7. If repeat set go to step 1.\n";
+
+
+        private const string CS_CMD_WRKSPSYNC_NAME = "workspace_sync";
+        private const string CS_CMD_WRKSPSYNC_DESCR = "Download or upload documents from local directory from/to workspace. Can be run as contignous process.";
+
+        private const string CS_ARG_NAME_DIR = "directory";
+        private const string CS_ARG_HINT_DIR = "<sync_directory>";
+        private const string CS_ARG_DESC_DIR = "Defines sync directory, if not defined workspace directory will be created as workspace name.";
+
+        private const string CS_ARG_NAME_WD = "repeat";
+        private const string CS_ARG_HINT_WD = "<is_repeat>";
+        private const string CS_ARG_DESC_WD = "Flag if defined, enables contignous monitoring on sync directory for changes. If not defined process will be run once.";
+
+        private const string CS_ARG_NAME_DELETE = "delete";
+        private const string CS_ARG_HINT_DELETE = "<is_delete_allowed>";
+        private const string CS_ARG_DESC_DELETE = "Flag if defined, deletes documents in application server when document is deleted in sync directory.";
+
+        private const string CS_ARG_NAME_INIT = "init";
+        private const string CS_ARG_HINT_INIT = "<is_initialization>";
+        private const string CS_ARG_DESC_INIT = "Flag if defined, reset sync directory during initialization (all content in sync directory will be deleted on start).";
+
+        private const string CS_ARG_NAME_INT = "interval";
+        private const string CS_ARG_HINT_INT = "<repeat_interval>";
+        private const string CS_ARG_DESC_INT = "Timespan interval between two iterations. Default is 00:30:00.";
+
+
+        #endregion
+
+        #region *** Properties ***
+        public override string Name => CS_CMD_WRKSPSYNC_NAME;
+        public override string Description => CS_CMD_WRKSPSYNC_DESCR;
+
+        public override string Notes => CS_NOTES;
+
+
+        private string SyncDir { get; set; }
+        private bool IsRepeat { get; set; }
+        private bool IsInit { get; set; }
+        private bool IsDeleteAllowed { get; set; }
+
+        private TimeSpan Interval { get; set; }
+        #endregion
+
+
+        #region *** Constructor ***
+
+        public WorkspaceSyncCommand(Engine engine) : base(engine)
+        {
+        }
+
+        #endregion
+
+        #region *** EXECUTE ***
+
+        protected override Result OnExecute()
+        {
+
+
+            bool isExitSignal = false;
+            int nCnt = 0;
+            while (!isExitSignal)
+            {
+                if (!IsRepeat)
+                    isExitSignal = true;
+
+                var workspace = Client.GetWorkspaceAsync(WorkspaceIdentificationTypeEnum.ApiKey, ApiKey).Result;
+                if (workspace == null)
+                    return new Result(new Exception($"Workspace with API-KEY: {ApiKey} not found."));
+                var documents = Client.GetWorkspaceDocumentsAsync(DocumentsScopeEnums.All, ApiKey, ApiKeyPassword, UserName).Result;
+                var dirInfo = OpenDirectory(workspace);
+
+                var factory = new DocumentEntryComparer();
+                foreach (var dir in dirInfo.EnumerateDirectories())
+                {
+                    if (string.Equals(dir.Name, CS_NEW_IN, StringComparison.CurrentCultureIgnoreCase))
+                    {
+                        foreach (var file in dir.GetFiles())
+                            factory.AddLocalDocumentNew(file.Name,true);
+
+                        continue;
+                    }
+
+                    if (string.Equals(dir.Name, CS_NEW_OUT, StringComparison.CurrentCultureIgnoreCase))
+                    {
+                        foreach (var file in dir.GetFiles())
+                            factory.AddLocalDocumentNew(file.Name,false);
+
+                        continue;
+                    }
+
+                    if (factory.ValidateDocumentDirectory(dir.Name))
+                    {
+
+                        foreach (var file in dir.GetFiles())
+                        {
+
+                            if (factory.ValidateArtifactFileName(file.Name))
+                            {
+                                factory.AddLocalDocument(dir.Name, file.Name, file.Length);
+                            }
+                            else
+                            {
+                                WriteWarning($"Invalid artifact file '{file.Name}' name format! Expects '<arifact_id>.<artifact_type>.<artifact_name>.<artifaxt_ext>'. Skip");
+                            }
+                        }
+                    }
+                    else
+                    {
+                        WriteWarning($"Invalid document directory '{dir.Name}' name format! Expects '<documentId:8>.<document_name_normalized>'. Skip");
+                    }
+                }
+
+                foreach (var document in documents)
+                {
+                    factory.AddRemoteDocument(document);
+                }
+
+                var diff = factory.GetComparator();
+                var locals = diff.CompareLeftSide().Where(x => x.CompareState != DiffEntry.CompareStateEnum.Equals);
+                foreach (var local in locals)
+                {
+                    var item = local.GetItem<DocumentItem>();
+                    if (local.CompareState == DiffEntry.CompareStateEnum.CurrentNull ||
+                        local.CompareState == DiffEntry.CompareStateEnum.CurrentSmaller)
+                    {
+                        WriteInfo($"Artifact '{item.ArtName}' will be downloaded");
+                    }
+                    else if (local.CompareState == DiffEntry.CompareStateEnum.CurrentGrater ||
+                             local.CompareState == DiffEntry.CompareStateEnum.OtherNull)
+                    {
+                        WriteInfo($"Artifact '{item.ArtName}' will be uploaded");
+                    }
+                }
+
+            }
+
+            
+            return new Result();
+        }
+
+
+        private IDirectoryInfo OpenDirectory(WorkspaceRDto workspace)
+        {
+            if (workspace == null)
+                throw new ArgumentNullException(nameof(workspace));
+
+            if (string.IsNullOrEmpty(SyncDir))
+            {
+                // Current dir
+                SyncDir = FileSystem.Path.Combine(FileSystem.Directory.GetCurrentDirectory(), FileUtils.SanitizedFileName(FileSystem, workspace.Name));
+            }
+
+            if (IsInit)
+            {
+                if (FileSystem.Directory.Exists(SyncDir))
+                {
+                    FileSystem.Directory.Delete(SyncDir,true);
+                    WriteDebugInfo($"Directory '{SyncDir}' deleted due 'init' mode.");
+                }
+
+                IsInit = false;
+            }
+
+
+            if (!FileSystem.Directory.Exists(SyncDir))
+            {
+                FileSystem.Directory.CreateDirectory(SyncDir);
+                WriteInfo($"Sync directory '{SyncDir}' created.");
+
+                var inDir = FileSystem.Path.Combine(SyncDir, CS_NEW_IN);
+                FileSystem.Directory.CreateDirectory(inDir);
+                WriteDebugInfo($"Directory '{inDir}' created.");
+                inDir = FileSystem.Path.Combine(SyncDir, CS_NEW_OUT);
+                FileSystem.Directory.CreateDirectory(inDir);
+                WriteDebugInfo($"Directory '{inDir}' created.");
+            }
+
+            var di = FileSystem.DirectoryInfo.FromDirectoryName(SyncDir);
+            WriteCaption($"Sync directory '{SyncDir}' opened.");
+            return di;
+        }
+
+        #endregion
+
+        #region *** Private operations ***
+
+        protected override void OnValidateArguments()
+        {
+            base.OnValidateArguments();
+
+            SyncDir = GetArgumentValueOrDefault<string>(CS_ARG_NAME_DIR);
+            IsRepeat = GetArgumentValueOrDefault<bool>(CS_ARG_NAME_WD);
+            IsInit = GetArgumentValueOrDefault<bool>(CS_ARG_NAME_INIT);
+            IsDeleteAllowed = GetArgumentValueOrDefault<bool>(CS_ARG_NAME_DELETE);
+            Interval = GetArgumentValueOrDefault<TimeSpan>(CS_ARG_NAME_INT);
+        }
+
+        protected override IEnumerable<AbstractArgument> OnSetupArguments()
+        {
+            var args = base.OnSetupArguments().ToList();
+            args.Add(new NamedArgument(CS_ARG_NAME_DIR, CS_ARG_DESC_DIR, CS_ARG_HINT_DIR, TypeValuesEnum.String, string.Empty, false));
+            args.Add(new FlagArgument(CS_ARG_NAME_WD, CS_ARG_DESC_WD, CS_ARG_HINT_WD,  false));
+            args.Add(new NamedArgument(CS_ARG_NAME_INT, CS_ARG_DESC_INT, CS_ARG_HINT_INT, TypeValuesEnum.TimeSpan, TimeSpan.FromSeconds(30).ToString(), false));
+            args.Add(new FlagArgument(CS_ARG_NAME_DELETE, CS_ARG_DESC_DELETE, CS_ARG_HINT_DELETE,  false));
+            args.Add(new FlagArgument(CS_ARG_NAME_INIT, CS_ARG_DESC_INIT, CS_ARG_HINT_INIT,  false));
+            return args;
+        }
+
+        #endregion
+    }
+}

+ 1 - 1
Console/Properties/launchSettings.json

@@ -2,7 +2,7 @@
   "profiles": {
     "Console": {
       "commandName": "Project",
-      "commandLineArgs": "document_get -name:doc1_file1 -user:test -dbg -force"
+      "commandLineArgs": "workspace_sync -directory:\"D:\\BO\" -init"
     }
   }
 }

+ 5 - 5
Console/console.json

@@ -1,14 +1,14 @@
 {
-  "api-base-url": "https://localhost:5001/api/",
+  "api-base-url": "http://192.168.1.101:5000/api/",
   "api-user": "Console",
   "api-pwd": "A1Abastr0vA",
   "api-magic": "6DEC4167-57A6-4333-92DA-ADE009A97DE9",
   "api-timeout": "00:10:00",
   "default-page-size": 50,
-  "defaults" : {
+  "defaults": {
     "workspace": {
-      "api-key": "33511A7C-9F05-4058-A840-85A589973BC2",
+      "api-key": "94866BC2-2F3E-4ECD-A8EE-61356A54FB41",
       "api-key-password": "test"
-    } 
-  } 
+    }
+  }
 }