WorkspaceSyncCommand.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.ComponentModel.Design;
  5. using System.IO;
  6. using System.IO.Abstractions;
  7. using System.Linq;
  8. using System.Threading;
  9. using BO.AppServer.Metadata.Dto;
  10. using BO.AppServer.Metadata.Enums;
  11. using BO.Console.Commands.Base;
  12. using BO.Console.Commands.Workspace.Sync;
  13. using BO.Console.Commands.Workspace.Sync.Diff;
  14. using Quadarax.Foundation.Core.Data.Diff;
  15. using Quadarax.Foundation.Core.IO;
  16. using Quadarax.Foundation.Core.IO.Extensions;
  17. using Quadarax.Foundation.Core.QConsole;
  18. using Quadarax.Foundation.Core.QConsole.Argument;
  19. using Quadarax.Foundation.Core.QConsole.Attributes;
  20. using Quadarax.Foundation.Core.QConsole.Value;
  21. using Quadarax.Foundation.Core.Value;
  22. using Quadarax.Foundation.Core.Value.Extensions;
  23. namespace BO.Console.Commands.Workspace
  24. {
  25. [CommandDefinition]
  26. internal class WorkspaceSyncCommand : AbstractWorkspaceCommand
  27. {
  28. #region *** Constants ***
  29. private const string CS_NEW_IN = ".in";
  30. private const string CS_NEW_OUT = ".out";
  31. private const string CS_NOTES = "Directory structure:\n" +
  32. "["+CS_NEW_IN + "]\t- new artifacts directory for Input type\n" +
  33. "\t\t<doc_name>.<artifact_name>.<artifact_ext> - new artifacts file\n" +
  34. "\t\t...\n" +
  35. "[" +CS_NEW_OUT + "]\t- new artifacts directory for Output type\n" +
  36. "[<documentId:8>.<document_name_normalized>]\t- document directory\n" +
  37. "\t\t<arifact_id:8>.<artifact_type>.<artifact_name>.<artifaxt_ext> - artifact file\n" +
  38. "\t\t...\n" +
  39. "...\n" +
  40. "\n" +
  41. "Process sequence:\n" +
  42. "0. If init flag set, sync clean directory\n" +
  43. "1. Get workspace documents with artifacts -> srv_arts\n" +
  44. "2. Get sync directory documents with artifacts (skip not well formatted, process incomming also) -> loac_arts\n" +
  45. "3. Resolve differences srv_arts and loc_ars -> changed_arts\n" +
  46. "4. Push changes changed_arts to database (if delete flag set removes documents in database)\n" +
  47. "5. Pull changes changed_arts from database (if delete flag set removes documents in sync directory)\n" +
  48. "6. Cleanup incommings\n" +
  49. "7. If repeat set go to step 1.\n";
  50. private const string CS_CMD_WRKSPSYNC_NAME = "workspace_sync";
  51. private const string CS_CMD_WRKSPSYNC_DESCR = "Download or upload documents from local directory from/to workspace. Can be run as contignous process.";
  52. private const string CS_ARG_NAME_DIR = "directory";
  53. private const string CS_ARG_HINT_DIR = "<sync_directory>";
  54. private const string CS_ARG_DESC_DIR = "Defines sync directory, if not defined workspace directory will be created as workspace name.";
  55. private const string CS_ARG_NAME_WD = "repeat";
  56. private const string CS_ARG_HINT_WD = "<is_repeat>";
  57. 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.";
  58. private const string CS_ARG_NAME_DELETE = "delete";
  59. private const string CS_ARG_HINT_DELETE = "<is_delete_allowed>";
  60. private const string CS_ARG_DESC_DELETE = "Flag if defined, deletes documents in application server when document is deleted in sync directory.";
  61. private const string CS_ARG_NAME_INIT = "init";
  62. private const string CS_ARG_HINT_INIT = "<is_initialization>";
  63. 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).";
  64. private const string CS_ARG_NAME_INT = "interval";
  65. private const string CS_ARG_HINT_INT = "<repeat_interval>";
  66. private const string CS_ARG_DESC_INT = "Timespan interval between two iterations. Default is 00:30:00.";
  67. 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.";
  68. #endregion
  69. #region *** Properties ***
  70. public override string Name => CS_CMD_WRKSPSYNC_NAME;
  71. public override string Description => CS_CMD_WRKSPSYNC_DESCR;
  72. public override string Notes => CS_NOTES;
  73. private string SyncDir { get; set; }
  74. private bool IsRepeat { get; set; }
  75. private bool IsInit { get; set; }
  76. private bool IsDeleteAllowed { get; set; }
  77. private TimeSpan Interval { get; set; }
  78. #endregion
  79. #region *** Constructor ***
  80. public WorkspaceSyncCommand(Engine engine) : base(engine)
  81. {
  82. }
  83. #endregion
  84. #region *** EXECUTE ***
  85. protected override Result OnExecute()
  86. {
  87. bool isExitSignal = false;
  88. int nCnt = 0;
  89. if (!string.IsNullOrEmpty(UserName))
  90. {
  91. WriteDebugInfo($"Getting default workspace info for user '{UserName}'");
  92. var workspaces = Client.GetWorkspacesAsync(WorkspaceScopeEnums.MyDefault, UserName).Result;
  93. if (!workspaces.Any())
  94. throw new Exception($"No default workspace for user '{UserName}' set.");
  95. var userWorkspace = workspaces.First();
  96. WriteInfo($"User has default workspace named '{userWorkspace.Name}' [{userWorkspace.Id}] (Created:{userWorkspace.Audit.Created.ToFileShortDateTimeString()}, Changed:{userWorkspace.Audit.Changed?.ToFileShortDateTimeString()})");
  97. WriteCaption($"Using APIKey: {userWorkspace.ApiKeyNormalized}");
  98. SetApiKey(userWorkspace.ApiKeyNormalized, userWorkspace.Apipassword);
  99. }
  100. var workspace = Client.GetWorkspaceAsync(WorkspaceIdentificationTypeEnum.ApiKey, ApiKey).Result;
  101. if (workspace == null)
  102. return new Result(new Exception($"Workspace with API-KEY: {ApiKey} not found."));
  103. var dirInfo = OpenDirectory(workspace);
  104. var iteration = 0;
  105. while (!isExitSignal)
  106. {
  107. if (!IsRepeat)
  108. isExitSignal = true;
  109. WriteInfo($"Iteration {iteration} started.");
  110. var documents = Client.GetWorkspaceDocumentsAsync(DocumentsScopeEnums.All, ApiKey, ApiKeyPassword, UserName).Result;
  111. var factory = new DocumentEntryComparer();
  112. foreach (var dir in dirInfo.EnumerateDirectories())
  113. {
  114. if (string.Equals(dir.Name, CS_NEW_IN, StringComparison.CurrentCultureIgnoreCase))
  115. {
  116. foreach (var file in dir.GetFiles())
  117. factory.AddLocalDocumentNew(file.Name,true);
  118. continue;
  119. }
  120. if (string.Equals(dir.Name, CS_NEW_OUT, StringComparison.CurrentCultureIgnoreCase))
  121. {
  122. foreach (var file in dir.GetFiles())
  123. factory.AddLocalDocumentNew(file.Name,false);
  124. continue;
  125. }
  126. if (factory.ValidateDocumentDirectory(dir.Name))
  127. {
  128. foreach (var file in dir.GetFiles())
  129. {
  130. if (factory.ValidateArtifactFileName(file.Name))
  131. {
  132. factory.AddLocalDocument(dir.Name, file.Name, file.Length);
  133. }
  134. else
  135. {
  136. WriteWarning($"Invalid artifact file '{file.Name}' name format! Expects '<arifact_id>.<artifact_type>.<artifact_name>.<artifaxt_ext>'. Skip");
  137. }
  138. }
  139. }
  140. else
  141. {
  142. WriteWarning($"Invalid document directory '{dir.Name}' name format! Expects '<documentId:8>.<document_name_normalized>'. Skip");
  143. }
  144. }
  145. foreach (var document in documents)
  146. {
  147. factory.AddRemoteDocument(document);
  148. }
  149. var diff = factory.GetComparator();
  150. var locals = diff.CompareRightSide().Where(x => x.CompareState != DiffEntry.CompareStateEnum.Equals);
  151. foreach (var local in locals)
  152. {
  153. var item = local.GetItem<DocumentItem>();
  154. if (local.CompareState == DiffEntry.CompareStateEnum.CurrentNull ||
  155. local.CompareState == DiffEntry.CompareStateEnum.CurrentSmaller)
  156. {
  157. WriteInfo($"Artifact '{item.ArtName}' will be uploaded");
  158. }
  159. else if (local.CompareState == DiffEntry.CompareStateEnum.CurrentGrater ||
  160. local.CompareState == DiffEntry.CompareStateEnum.OtherNull)
  161. {
  162. WriteInfo($"Artifact '{item.ArtName}' will be downloaded");
  163. DownloadArtifact(dirInfo, item);
  164. }
  165. }
  166. if (IsRepeat)
  167. Thread.CurrentThread.Join(Interval);
  168. }
  169. return new Result();
  170. }
  171. private IDirectoryInfo OpenDirectory(WorkspaceRDto workspace)
  172. {
  173. if (workspace == null)
  174. throw new ArgumentNullException(nameof(workspace));
  175. if (string.IsNullOrEmpty(SyncDir))
  176. {
  177. // Current dir
  178. SyncDir = FileSystem.Path.Combine(FileSystem.Directory.GetCurrentDirectory(), FileUtils.SanitizedFileName(FileSystem, workspace.Name));
  179. }
  180. if (IsInit)
  181. {
  182. if (FileSystem.Directory.Exists(SyncDir))
  183. {
  184. FileSystem.Directory.Delete(SyncDir,true);
  185. WriteDebugInfo($"Directory '{SyncDir}' deleted due 'init' mode.");
  186. }
  187. IsInit = false;
  188. }
  189. if (!FileSystem.Directory.Exists(SyncDir))
  190. {
  191. FileSystem.Directory.CreateDirectory(SyncDir);
  192. WriteInfo($"Sync directory '{SyncDir}' created.");
  193. var inDir = FileSystem.Path.Combine(SyncDir, CS_NEW_IN);
  194. FileSystem.Directory.CreateDirectory(inDir);
  195. WriteDebugInfo($"Directory '{inDir}' created.");
  196. inDir = FileSystem.Path.Combine(SyncDir, CS_NEW_OUT);
  197. FileSystem.Directory.CreateDirectory(inDir);
  198. WriteDebugInfo($"Directory '{inDir}' created.");
  199. }
  200. var di = FileSystem.DirectoryInfo.FromDirectoryName(SyncDir);
  201. WriteCaption($"Sync directory '{SyncDir}' opened.");
  202. return di;
  203. }
  204. private void DownloadArtifact(IDirectoryInfo outputDir, DocumentItem artifactItem)
  205. {
  206. if (outputDir == null)
  207. throw new ArgumentNullException(nameof(outputDir));
  208. if (artifactItem == null)
  209. throw new ArgumentNullException(nameof(artifactItem));
  210. var currentDir = outputDir;
  211. using (var artFs = Client.GetWorkspaceDocumentContentAsync(ApiKey, ApiKeyPassword,
  212. artifactItem.DocId.GetValueOrDefault(), artifactItem.ArtId.GetValueOrDefault(),string.Empty).Result)
  213. {
  214. long fileSize = 0;
  215. var fileName = GetArtifactPhysicalFileName(artifactItem);
  216. var dirName = GetDocumentPhysicalDirName(artifactItem);
  217. currentDir = currentDir.GetDirectory(dirName) ?? outputDir.CreateSubdirectory(dirName);
  218. using (var outFs = currentDir.CreateFile(fileName))
  219. {
  220. //CopyStream(artFs, outFs);
  221. artFs.CopyTo(outFs);
  222. outFs.Flush();
  223. fileSize = outFs.Length;
  224. }
  225. WriteInfo($"{fileName} downloaded [{artifactItem.ArtLength}/{fileSize} bytes]");
  226. }
  227. }
  228. public static void CopyStream(Stream input, Stream output)
  229. {
  230. byte[] buffer = new byte[16 * 1024];
  231. int read;
  232. while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
  233. {
  234. output.Write(buffer, 0, read);
  235. }
  236. }
  237. private string GetArtifactPhysicalFileName(DocumentItem artifactItem)
  238. {
  239. //<arifact_id:8>.<artifact_type>.<artifact_name>.<artifaxt_ext> - artifact file
  240. return $"{artifactItem.ArtId.GetValueOrDefault():D8}.{artifactItem.ArtType}.{FileUtils.SanitizedFileName(FileSystem,artifactItem.ArtName)}{artifactItem.ArtExt}";
  241. }
  242. private string GetDocumentPhysicalDirName(DocumentItem documentItem)
  243. {
  244. //[<documentId:8>.<document_name_normalized>]
  245. return $"{documentItem.DocId.GetValueOrDefault():D8}.{FileUtils.SanitizedFileName(FileSystem, documentItem.DocName)}";
  246. }
  247. #endregion
  248. #region *** Private operations ***
  249. protected override void OnValidateArguments()
  250. {
  251. base.OnValidateArguments();
  252. SyncDir = GetArgumentValueOrDefault<string>(CS_ARG_NAME_DIR);
  253. IsRepeat = GetArgumentValueOrDefault<bool>(CS_ARG_NAME_WD);
  254. IsInit = GetArgumentValueOrDefault<bool>(CS_ARG_NAME_INIT);
  255. IsDeleteAllowed = GetArgumentValueOrDefault<bool>(CS_ARG_NAME_DELETE);
  256. Interval = GetArgumentValueOrDefault<TimeSpan>(CS_ARG_NAME_INT);
  257. }
  258. protected override IEnumerable<AbstractArgument> OnSetupArguments()
  259. {
  260. var args = base.OnSetupArguments().ToList();
  261. args.Add(new NamedArgument(CS_ARG_NAME_DIR, CS_ARG_DESC_DIR, CS_ARG_HINT_DIR, TypeValuesEnum.String, string.Empty, false));
  262. args.Add(new FlagArgument(CS_ARG_NAME_WD, CS_ARG_DESC_WD, CS_ARG_HINT_WD, false));
  263. args.Add(new NamedArgument(CS_ARG_NAME_INT, CS_ARG_DESC_INT, CS_ARG_HINT_INT, TypeValuesEnum.TimeSpan, TimeSpan.FromSeconds(30).ToString(), false));
  264. args.Add(new FlagArgument(CS_ARG_NAME_DELETE, CS_ARG_DESC_DELETE, CS_ARG_HINT_DELETE, false));
  265. args.Add(new FlagArgument(CS_ARG_NAME_INIT, CS_ARG_DESC_INIT, CS_ARG_HINT_INIT, false));
  266. AppendUserArgument(args, CS_ARG_DESC_USER);
  267. return args;
  268. }
  269. #endregion
  270. }
  271. }