Эх сурвалжийг харах

WorkspaceSync in preliminary version

Dalibor Votruba 4 жил өмнө
parent
commit
68153fc1e3

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

@@ -47,6 +47,7 @@ namespace BO.AppServer.Business.Mapper
 
             DefineReverse<DocumentCDto, Metadocument>();
 
+            Define<Metadocument, DocumentRDto>();
             Define<Metadocument, DocumentRDto>(builder =>
             {
                 builder.Custom(dest => dest.Audit, src => new BoAuditDto()
@@ -60,7 +61,7 @@ namespace BO.AppServer.Business.Mapper
                 //calculate on demand  builder.Custom(dest => dest.BillingPlanCode, src => src.WorkspaceBillings.OrderByDescending(x=>x.Created).FirstOrDefault().BillingPlan.Code);
             });
             
-            Define<ArtifactCDto, Artifact>();
+            //Define<ArtifactCDto, Artifact>();
             Define<Artifact, ArtifactRDto>(builder =>
             {
                 builder.Custom(dest => dest.Audit, src => new BoAuditDto()

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

@@ -1,4 +1,5 @@
-using Quadarax.Foundation.Core.Data.Interface.Entity;
+using BO.AppServer.Metadata.Enums;
+using Quadarax.Foundation.Core.Data.Interface.Entity;
 
 namespace BO.AppServer.Metadata.Dto
 {
@@ -39,6 +40,7 @@ namespace BO.AppServer.Metadata.Dto
         public string MimeType { get; set;}
         public string MimeTypeDescription { get; set;}
         public long Length { get; set;}
+        public ArtifactTypeEnum Type { get; set; }
     }
 
     #endregion

+ 1 - 1
AppServer/Metadata/Dto/DocumentDto.cs

@@ -9,7 +9,7 @@ namespace BO.AppServer.Metadata.Dto
 
     public class DocumentBaseDto : BoDto
     {
-        public string Name { get; }
+        public string Name { get; set; }
         public DocumentStatusEnum Status { get; set; }
     }
 

+ 9 - 0
Common/qdr.fnd.core.qconsole/Argument/AbstractArgument.cs

@@ -42,6 +42,10 @@ namespace Quadarax.Foundation.Core.QConsole.Argument
         /// Default value of argument, uses if argument is not specified.
         /// </summary>
         public AbstractValue DefaultValue { get; }
+
+
+
+        public bool WasUsed { get; private set; }
         #endregion
 
         #region *** Constructors ***
@@ -71,6 +75,11 @@ namespace Quadarax.Foundation.Core.QConsole.Argument
                 Value.Set(DefaultValue.Get());
         }
 
+        public void Hit()
+        {
+            WasUsed = true;
+        }
+
         public override string ToString()
         {
             var sb = new StringBuilder();

+ 1 - 0
Common/qdr.fnd.core.qconsole/Argument/FlagArgument.cs

@@ -12,6 +12,7 @@ namespace Quadarax.Foundation.Core.QConsole.Argument
         public void Set()
         {
             Value.Set(!(bool)DefaultValue.Get());
+            Hit();
         }
 
         public void Reset()

+ 5 - 1
Common/qdr.fnd.core.qconsole/Command/Base/AbstractCommand.cs

@@ -131,7 +131,10 @@ namespace Quadarax.Foundation.Core.QConsole.Command.Base
                     if (argument.IsFlag())
                         ((FlagArgument)argument).Set();
                     else
+                    {
                         argument.Value.Set(parsed.Item2);
+                        argument.Hit();
+                    }
                 }
                 else
                 {
@@ -143,6 +146,7 @@ namespace Quadarax.Foundation.Core.QConsole.Command.Base
                             $"Invalid ordinal argument '{arg}' at position {index} for this command.");
 
                     argument.Value.Set(arg);
+                    argument.Hit();
                     WriteDebugInfo($"Ordinal argument '{argument.Code}' is set to value '{argument.Value.AsString()}'.");
                 }
             }
@@ -359,7 +363,7 @@ namespace Quadarax.Foundation.Core.QConsole.Command.Base
         {
             if (!ContainsArgument(code)) throw new ArgumentOutOfRangeException(code, $"Argument '{code}' not defined.");
             var arg = GetArgument(code);
-            return arg.Value.HasValue;
+            return arg.Value.HasValue && arg.WasUsed;
         }
 
         protected void ValidateArgumentValueEnum(string argumentName, Type enumType)

+ 113 - 0
Common/qdr.fnd.core/Console/ProgressBar.cs

@@ -0,0 +1,113 @@
+using System;
+using System.Text;
+using System.Threading;
+using Quadarax.Foundation.Core.Object;
+// ReSharper disable InconsistentNaming
+
+namespace Quadarax.Foundation.Core.Console
+{
+    public class ProgressBar : DisposableObject, IProgress<double>
+    {
+
+        #region *** Constants ***
+        private const int CN_BLOCK_COUNT = 10;
+        private const string CS_ANIMATION = @"|/-\";
+        #endregion
+        #region *** Private fields ***
+        private readonly TimeSpan _animationInterval = TimeSpan.FromSeconds(1.0 / 8);
+        private readonly Timer _timer;
+        private double _currentProgress;
+        private string _currentText = string.Empty;
+        private int _animationIndex;
+        private ConsoleWriter _writer;
+        #endregion
+
+        #region *** Constructors ***
+        public ProgressBar(ConsoleWriter writer)
+        {
+            _writer = writer ?? throw new ArgumentNullException(nameof(writer));
+            _timer = new Timer(TimerHandler);
+
+            // A progress bar is only for temporary display in a console window.
+            // If the console output is redirected to a file, draw nothing.
+            // Otherwise, we'll end up with a lot of garbage in the target file.
+            if (!System.Console.IsOutputRedirected)
+            {
+                ResetTimer();
+            }
+        }
+        #endregion
+
+        #region *** Public Operations ***
+        /// <summary>
+        /// Update progress status.
+        /// </summary>
+        /// <param name="value">Status value between 0 - 1. Represents percentage.</param>
+        public void Report(double value)
+        {
+            // Make sure value is in [0..1] range
+            value = Math.Max(0, Math.Min(1, value));
+            Interlocked.Exchange(ref _currentProgress, value);
+        }
+        #endregion
+
+        #region *** Private Operations ***
+        private void TimerHandler(object state)
+        {
+            lock (_timer)
+            {
+                var progressBlockCount = (int)(_currentProgress * CN_BLOCK_COUNT);
+                var percent = (int)(_currentProgress * 100);
+                var text = $"[{new string('#', progressBlockCount)}{new string('-', CN_BLOCK_COUNT - progressBlockCount)}] {percent,3}% {CS_ANIMATION[_animationIndex++ % CS_ANIMATION.Length]}";
+                UpdateText(text);
+                ResetTimer();
+            }
+        }
+
+        private void UpdateText(string text)
+        {
+            // Get length of common portion
+            var commonPrefixLength = 0;
+            var commonLength = Math.Min(_currentText.Length, text.Length);
+            while (commonPrefixLength < commonLength && text[commonPrefixLength] == _currentText[commonPrefixLength])
+            {
+                commonPrefixLength++;
+            }
+
+            // Backtrack to the first differing character
+            var outputBuilder = new StringBuilder();
+            outputBuilder.Append('\b', _currentText.Length - commonPrefixLength);
+
+            // Output new suffix
+            outputBuilder.Append(text.Substring(commonPrefixLength));
+
+            // If the new text is shorter than the old one: delete overlapping characters
+            var overlapCount = _currentText.Length - text.Length;
+            if (overlapCount > 0)
+            {
+                outputBuilder.Append(' ', overlapCount);
+                outputBuilder.Append('\b', overlapCount);
+            }
+
+            _writer.Write(outputBuilder.ToString());
+            _currentText = text;
+        }
+
+        private void ResetTimer()
+        {
+            _timer.Change(_animationInterval, TimeSpan.FromMilliseconds(-1));
+        }
+        #endregion
+
+        #region *** Private Overrides ***
+        protected override void OnDisposing()
+        {
+            lock (_timer)
+            {
+                UpdateText(string.Empty);
+                _writer = null;
+            }
+        }
+        #endregion
+    }
+}

+ 59 - 0
Common/qdr.fnd.core/IO/Extensions/DirectoryInfoExt.cs

@@ -0,0 +1,59 @@
+using System;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+
+namespace Quadarax.Foundation.Core.IO.Extensions
+{
+    public static class DirectoryInfoExt
+    {
+        public static Stream CreateFile(this IDirectoryInfo dirInfo, string fileNameWithExtension, bool forceOverwrite = false)
+        {
+            if (dirInfo == null)
+                throw new ArgumentNullException(nameof(dirInfo));
+            if (string.IsNullOrEmpty(fileNameWithExtension))
+                throw new ArgumentNullException(nameof(fileNameWithExtension));
+
+            if (forceOverwrite)
+            {
+
+                var fileInfo = dirInfo.GetFiles(fileNameWithExtension).FirstOrDefault();
+                fileInfo?.Delete();
+            }
+
+            var fullFileName = dirInfo.FileSystem.Path.Combine(dirInfo.FullName, fileNameWithExtension);
+            return dirInfo.FileSystem.File.OpenWrite(fullFileName);
+        }
+
+        public static bool ExistsFile(this IDirectoryInfo dirInfo, string fileNameWithExtension)
+        {
+            if (dirInfo == null)
+                throw new ArgumentNullException(nameof(dirInfo));
+            if (string.IsNullOrEmpty(fileNameWithExtension))
+                throw new ArgumentNullException(nameof(fileNameWithExtension));
+
+            return dirInfo.GetFiles(fileNameWithExtension).Any();
+        }
+
+        public static bool Exists(this IDirectoryInfo dirInfo, string subDirectoryName)
+        {
+            if (dirInfo == null)
+                throw new ArgumentNullException(nameof(dirInfo));
+            if (string.IsNullOrEmpty(subDirectoryName))
+                throw new ArgumentNullException(nameof(subDirectoryName));
+
+            return dirInfo.Exists && dirInfo.GetDirectories(subDirectoryName, SearchOption.TopDirectoryOnly).Any();
+        }
+
+        public static IDirectoryInfo GetDirectory(this IDirectoryInfo dirInfo, string subDirectoryName)
+        {
+            if (dirInfo == null)
+                throw new ArgumentNullException(nameof(dirInfo));
+            if (string.IsNullOrEmpty(subDirectoryName))
+                throw new ArgumentNullException(nameof(subDirectoryName));
+
+            return dirInfo.GetDirectories(subDirectoryName, SearchOption.TopDirectoryOnly).FirstOrDefault();
+        }
+
+    }
+}

+ 0 - 0
Common/qdr.fnd.core/IO/StreamExt.cs → Common/qdr.fnd.core/IO/Extensions/StreamExt.cs


+ 18 - 2
Console/Commands/Base/AbstractWorkspaceCommand.cs

@@ -52,7 +52,8 @@ namespace BO.Console.Commands.Base
             if (string.IsNullOrEmpty(ApiKey) || string.IsNullOrEmpty(ApiKeyPassword))
                 throw new ArgumentException(
                     $"ApiKey or ApiKeyPassword must be specified by argument {Engine.Configuration.CharacterNamedArgumentSeparator}{CS_ARG_NAME_APIK} or {Engine.Configuration.CharacterNamedArgumentSeparator}{CS_ARG_NAME_APIKP} or specified in configuration file {Constants.CS_CONFIGURATION_FILE} in location 'defaults/workspace/api-key' or 'defaults/workspace/api-key-password'.");
-
+            
+            WriteDebugInfo($"ApiKey and password was set as '{ApiKey}'");
             WriteInfo($"Using API-KEY: {ApiKey}");
             WriteDebugInfo($"Using API-KEY-PASSWORD: {ApiKeyPassword}");
 
@@ -64,8 +65,23 @@ namespace BO.Console.Commands.Base
             args.Add(new NamedArgument(CS_ARG_NAME_APIK, CS_ARG_DESC_APIK,CS_ARG_HINT_APIK, TypeValuesEnum.String, string.Empty, false));
             args.Add(new NamedArgument(CS_ARG_NAME_APIKP, CS_ARG_DESC_APIKP,CS_ARG_HINT_APIKP, TypeValuesEnum.String, string.Empty, false));
             return args;
-        }
+        }
+
+        #endregion
+
+        #region *** Protected Operations ***
 
+        protected void SetApiKey(string apiKey, string apiPassword)
+        {
+            if (string.IsNullOrEmpty(apiKey))
+                throw new ArgumentNullException(nameof(apiKey));
+            if (string.IsNullOrEmpty(apiPassword))
+                throw new ArgumentNullException(nameof(apiPassword));
+
+            ApiKey = apiKey;
+            ApiKeyPassword = apiPassword;
+            WriteDebugInfo($"ApiKey and password was set as '{ApiKey}'");
+        }
         #endregion
     }
 }

+ 7 - 1
Console/Commands/Base/BaseLocalCommand.cs

@@ -37,6 +37,8 @@ namespace BO.Console.Commands.Base
 
         protected bool? IsForce { get; private set; }
 
+        protected bool IsDebug { get; private set; }
+
         protected IFileSystem FileSystem { get; private set; }
 
         protected BaseLocalCommand(Engine engine) : base(engine)
@@ -61,7 +63,9 @@ namespace BO.Console.Commands.Base
                     WriteWarning("Force flag is set!");
             }
 
-            if (WasArgumentSpecified(CS_ARG_NAME_DEBUG))
+            IsDebug = WasArgumentSpecified(CS_ARG_NAME_DEBUG);
+
+            if (IsDebug)
                 DebugConsoleWriter.IsWriteDebugEnabled = true;
         }
 
@@ -82,6 +86,8 @@ namespace BO.Console.Commands.Base
 
         public void Log(LogSeverityEnum type, string message, Exception e = null)
         {
+            if ((type == LogSeverityEnum.Trace || type == LogSeverityEnum.Debug) && !IsDebug)
+                return;
             WriteInfo($"{type}: {message}" + (e == null ? "" : e.Message));
         }
 

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

@@ -18,6 +18,16 @@ namespace BO.Console.Commands.Workspace.Sync.Diff
 
         protected override CompareStateEnum OnCompareEquality(DiffEntry other)
         {
+            if (other == null || other.Item == null)
+                return CompareStateEnum.OtherNull;
+
+            var itemCurrent = (DocumentItem)Item;
+            var itemOther = (DocumentItem)other.Item;
+
+            if (itemCurrent.ArtLength == itemOther.ArtLength)
+                return CompareStateEnum.Equals;
+
+
             return CompareStateEnum.CurrentSmaller;
         }
     }

+ 5 - 1
Console/Commands/Workspace/Sync/Diff/DocumentItem.cs

@@ -1,4 +1,6 @@
-namespace BO.Console.Commands.Workspace.Sync.Diff
+using BO.AppServer.Metadata.Enums;
+
+namespace BO.Console.Commands.Workspace.Sync.Diff
 {
     internal class DocumentItem
     {
@@ -13,5 +15,7 @@
         public long ArtLength { get; set; }
 
         public string PhysicalPath { get; set; }
+
+        public ArtifactTypeEnum ArtType { get; set; }
     }
 }

+ 4 - 1
Console/Commands/Workspace/Sync/DocumentEntryComparer.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using BO.AppServer.Metadata.Dto;
+using BO.AppServer.Metadata.Enums;
 using BO.Console.Commands.Workspace.Sync.Diff;
 using Quadarax.Foundation.Core.Data.Diff;
 
@@ -31,8 +32,9 @@ namespace BO.Console.Commands.Workspace.Sync
                     item.ArtLength = fileSize;
                     item.DocId = doc.Item1;
                     item.DocName = doc.Item2;
+                    item.ArtType = art.Item2.ToLower() == "input" ? ArtifactTypeEnum.Input : ArtifactTypeEnum.Output;
                     item.IsLocal = true;
-                    _remote.Add(item);
+                    _local.Add(item);
                 }
             }
         }
@@ -53,6 +55,7 @@ namespace BO.Console.Commands.Workspace.Sync
                 item.DocId = document.Id;
                 item.DocName = document.Name;
                 item.IsLocal = false;
+                art.Type = art.Type;
                 _remote.Add(item);
             }
         }

+ 85 - 10
Console/Commands/Workspace/WorkspaceSyncCommand.cs

@@ -2,8 +2,10 @@
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.ComponentModel.Design;
+using System.IO;
 using System.IO.Abstractions;
 using System.Linq;
+using System.Threading;
 using BO.AppServer.Metadata.Dto;
 using BO.AppServer.Metadata.Enums;
 using BO.Console.Commands.Base;
@@ -11,11 +13,13 @@ 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.IO.Extensions;
 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;
+using Quadarax.Foundation.Core.Value.Extensions;
 
 namespace BO.Console.Commands.Workspace
 {
@@ -33,7 +37,7 @@ namespace BO.Console.Commands.Workspace
                                         "\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<arifact_id:8>.<artifact_type>.<artifact_name>.<artifaxt_ext> - artifact file\n" +
                                         "\t\t...\n" +
                                         "...\n" +
                                         "\n" +
@@ -71,6 +75,7 @@ namespace BO.Console.Commands.Workspace
         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.";
 
+        private const string CS_ARG_DESC_USER = "If defined, gets APIKey and password from defined user default workspace, otherwise use APIkey from argument or configuration.";
 
         #endregion
 
@@ -103,20 +108,38 @@ namespace BO.Console.Commands.Workspace
         protected override Result OnExecute()
         {
 
-
             bool isExitSignal = false;
             int nCnt = 0;
+
+            if (!string.IsNullOrEmpty(UserName))
+            {
+                WriteDebugInfo($"Getting default workspace info for user '{UserName}'");
+                var workspaces = Client.GetWorkspacesAsync(WorkspaceScopeEnums.MyDefault, UserName).Result;
+                if (!workspaces.Any())
+                    throw new Exception($"No default workspace for user '{UserName}' set.");
+                var userWorkspace = workspaces.First();
+                WriteInfo($"User has default workspace named '{userWorkspace.Name}' [{userWorkspace.Id}] (Created:{userWorkspace.Audit.Created.ToFileShortDateTimeString()}, Changed:{userWorkspace.Audit.Changed?.ToFileShortDateTimeString()})");
+                WriteCaption($"Using APIKey: {userWorkspace.ApiKeyNormalized}");
+                SetApiKey(userWorkspace.ApiKeyNormalized, userWorkspace.Apipassword);
+            }
+
+
+            var workspace = Client.GetWorkspaceAsync(WorkspaceIdentificationTypeEnum.ApiKey, ApiKey).Result;
+            if (workspace == null)
+                return new Result(new Exception($"Workspace with API-KEY: {ApiKey} not found."));
+            var dirInfo = OpenDirectory(workspace);
+
+            var iteration = 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);
+                WriteInfo($"Iteration {iteration} started.");
 
+                var documents = Client.GetWorkspaceDocumentsAsync(DocumentsScopeEnums.All, ApiKey, ApiKeyPassword, UserName).Result;
+                
                 var factory = new DocumentEntryComparer();
                 foreach (var dir in dirInfo.EnumerateDirectories())
                 {
@@ -164,22 +187,26 @@ namespace BO.Console.Commands.Workspace
                 }
 
                 var diff = factory.GetComparator();
-                var locals = diff.CompareLeftSide().Where(x => x.CompareState != DiffEntry.CompareStateEnum.Equals);
+                var locals = diff.CompareRightSide().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");
+                        WriteInfo($"Artifact '{item.ArtName}' will be uploaded");
+                        
                     }
                     else if (local.CompareState == DiffEntry.CompareStateEnum.CurrentGrater ||
                              local.CompareState == DiffEntry.CompareStateEnum.OtherNull)
                     {
-                        WriteInfo($"Artifact '{item.ArtName}' will be uploaded");
+                        WriteInfo($"Artifact '{item.ArtName}' will be downloaded");
+                        DownloadArtifact(dirInfo, item);
                     }
                 }
 
+                if (IsRepeat)
+                    Thread.CurrentThread.Join(Interval);
             }
 
             
@@ -228,6 +255,53 @@ namespace BO.Console.Commands.Workspace
             return di;
         }
 
+
+        private void DownloadArtifact(IDirectoryInfo outputDir, DocumentItem artifactItem)
+        {
+            if (outputDir == null)
+                throw new ArgumentNullException(nameof(outputDir));
+            if (artifactItem == null)
+                throw new ArgumentNullException(nameof(artifactItem));
+            var currentDir = outputDir;
+            using (var artFs = Client.GetWorkspaceDocumentContentAsync(ApiKey, ApiKeyPassword,
+                artifactItem.DocId.GetValueOrDefault(), artifactItem.ArtId.GetValueOrDefault(),string.Empty).Result)
+            {
+                long fileSize = 0;
+                var fileName = GetArtifactPhysicalFileName(artifactItem);
+                var dirName = GetDocumentPhysicalDirName(artifactItem);
+                currentDir = currentDir.GetDirectory(dirName) ?? outputDir.CreateSubdirectory(dirName);
+                using (var outFs = currentDir.CreateFile(fileName))
+                {
+                    //CopyStream(artFs, outFs);
+                    artFs.CopyTo(outFs);
+                    outFs.Flush();
+                    fileSize = outFs.Length;
+                }
+                WriteInfo($"{fileName} downloaded [{artifactItem.ArtLength}/{fileSize} bytes]");
+            }
+
+        }
+        public static void CopyStream(Stream input, Stream output)
+        {
+            byte[] buffer = new byte[16 * 1024];
+            int read;
+            while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
+            {
+                output.Write(buffer, 0, read);
+            }
+        }
+        private string GetArtifactPhysicalFileName(DocumentItem artifactItem)
+        {
+            //<arifact_id:8>.<artifact_type>.<artifact_name>.<artifaxt_ext> - artifact file
+            return $"{artifactItem.ArtId.GetValueOrDefault():D8}.{artifactItem.ArtType}.{FileUtils.SanitizedFileName(FileSystem,artifactItem.ArtName)}{artifactItem.ArtExt}";
+        }
+
+        private string GetDocumentPhysicalDirName(DocumentItem documentItem)
+        {
+            //[<documentId:8>.<document_name_normalized>]
+            return $"{documentItem.DocId.GetValueOrDefault():D8}.{FileUtils.SanitizedFileName(FileSystem, documentItem.DocName)}";
+        }
+
         #endregion
 
         #region *** Private operations ***
@@ -251,6 +325,7 @@ namespace BO.Console.Commands.Workspace
             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));
+            AppendUserArgument(args, CS_ARG_DESC_USER);
             return args;
         }
 

+ 2 - 0
Console/Console.sln.DotSettings

@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Quadarax/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

+ 1 - 1
Console/Properties/launchSettings.json

@@ -2,7 +2,7 @@
   "profiles": {
     "Console": {
       "commandName": "Project",
-      "commandLineArgs": "help document_add"
+      "commandLineArgs": "workspace_sync -user:test -directory:\"D:\\BO\" -init -repeat -dbg"
     }
   }
 }

+ 8 - 0
Console/Samples.txt

@@ -0,0 +1,8 @@
+* user_getapikey
+- Get user 'test' API key and password
+user_getapikey -name:test
+
+* workspace_sync
+- Set sync directory repeatly (30s) (D:\BO\) with init for user 'test'.
+workspace_sync -user:test -directory:"D:\BO" -init -repeat
+