using System; 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; 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 { [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.. - new artifacts file\n" + "\t\t...\n" + "[" +CS_NEW_OUT + "]\t- new artifacts directory for Output type\n" + "[.]\t- document directory\n" + "\t\t... - 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 = ""; 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 = ""; 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 = ""; 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 = ""; 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 = ""; 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 #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; 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; WriteInfo($"Iteration {iteration} started."); var documents = Client.GetWorkspaceDocumentsAsync(DocumentsScopeEnums.All, ApiKey, ApiKeyPassword, UserName).Result; 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 '...'. Skip"); } } } else { WriteWarning($"Invalid document directory '{dir.Name}' name format! Expects '.'. Skip"); } } foreach (var document in documents) { factory.AddRemoteDocument(document); } var diff = factory.GetComparator(); var locals = diff.CompareRightSide().Where(x => x.CompareState != DiffEntry.CompareStateEnum.Equals); foreach (var local in locals) { var item = local.GetItem(); if (local.CompareState == DiffEntry.CompareStateEnum.CurrentNull || local.CompareState == DiffEntry.CompareStateEnum.CurrentSmaller) { 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 downloaded"); DownloadArtifact(dirInfo, item); } } if (IsRepeat) Thread.CurrentThread.Join(Interval); } 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; } 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) { //... - artifact file return $"{artifactItem.ArtId.GetValueOrDefault():D8}.{artifactItem.ArtType}.{FileUtils.SanitizedFileName(FileSystem,artifactItem.ArtName)}{artifactItem.ArtExt}"; } private string GetDocumentPhysicalDirName(DocumentItem documentItem) { //[.] return $"{documentItem.DocId.GetValueOrDefault():D8}.{FileUtils.SanitizedFileName(FileSystem, documentItem.DocName)}"; } #endregion #region *** Private operations *** protected override void OnValidateArguments() { base.OnValidateArguments(); SyncDir = GetArgumentValueOrDefault(CS_ARG_NAME_DIR); IsRepeat = GetArgumentValueOrDefault(CS_ARG_NAME_WD); IsInit = GetArgumentValueOrDefault(CS_ARG_NAME_INIT); IsDeleteAllowed = GetArgumentValueOrDefault(CS_ARG_NAME_DELETE); Interval = GetArgumentValueOrDefault(CS_ARG_NAME_INT); } protected override IEnumerable 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)); AppendUserArgument(args, CS_ARG_DESC_USER); return args; } #endregion } }