Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
FolderNode.cs
1using System;
3using System.IO;
4using System.Threading;
5using System.Threading.Tasks;
6using Waher.Events;
16
17namespace Waher.Things.Files
18{
23 {
27 NoSynchronization,
28
32 TopLevelOnly,
33
37 IncludeSubfolders
38 }
39
43 public class FolderNode : VirtualNode
44 {
45 private readonly SemaphoreSlim synchObj = new SemaphoreSlim(1);
46 private SynchronizationOptions synchronizationOptions = SynchronizationOptions.NoSynchronization;
47 private string folderPath;
48 private string fileFilter;
49 private Timer timer;
50
54 public FolderNode()
55 {
56 }
57
61 public override async Task DestroyAsync()
62 {
63 this.timer?.Dispose();
64 this.timer = null;
65
66 await FilesModule.StopSynchronization(this.folderPath);
67
68 await base.DestroyAsync();
69 }
70
74 [Page(2, "File System", 100)]
75 [Header(3, "Folder:")]
76 [ToolTip(4, "Full path to folder (on host).")]
77 public string FolderPath
78 {
79 get => this.folderPath;
80 set
81 {
82 if (this.folderPath != value)
83 {
84 this.folderPath = value;
85 this.CheckSynchronization();
86 }
87 }
88 }
89
93 [Page(2, "File System", 100)]
94 [Header(7, "Synchronization Mode:")]
95 [ToolTip(8, "If, and how, files in the folder (or subfolders) will be synchronized.")]
96 [Option(SynchronizationOptions.NoSynchronization, 9, "Do not synchronize files.")]
97 [Option(SynchronizationOptions.TopLevelOnly, 10, "Synchronize top-level files only.")]
98 [Option(SynchronizationOptions.IncludeSubfolders, 11, "Synchronize files in folder and subfolders.")]
99 [DefaultValue(SynchronizationOptions.NoSynchronization)]
100 [Text(TextPosition.AfterField, 16, "You can add default script templates to be used for files found, by adding string-valued meta-data tags to the node, where the meta-data key names correspond to file extensions.")]
102 {
103 get => this.synchronizationOptions;
104 set
105 {
106 if (this.synchronizationOptions != value)
107 {
108 this.synchronizationOptions = value;
109 this.CheckSynchronization();
110 }
111 }
112 }
113
117 [Page(2, "File System", 100)]
118 [Header(5, "File Filter:")]
119 [ToolTip(6, "You can limit the files to be monitored using a file filter. If no filter is provided, all files within the scope will be monitored.")]
120 public string FileFilter
121 {
122 get => this.fileFilter;
123 set
124 {
125 if (this.fileFilter != value)
126 {
127 this.fileFilter = value;
128 this.CheckSynchronization();
129 }
130 }
131 }
132
138 public override Task<string> GetTypeNameAsync(Language Language)
139 {
140 return Language.GetStringAsync(typeof(FolderNode), 1, "File Folder");
141 }
142
148 public override Task<bool> AcceptsChildAsync(INode Child)
149 {
150 return Task.FromResult(
151 Child is SubFolderNode ||
152 Child is FileNode);
153 }
154
160 public override Task<bool> AcceptsParentAsync(INode Parent)
161 {
162 return Task.FromResult(
163 Parent is Root ||
166 }
167
168 private void CheckSynchronization()
169 {
170 this.timer?.Dispose();
171 this.timer = null;
172
173 this.timer = new Timer(this.DelayedCheckSynchronization, null, 500, Timeout.Infinite);
174 }
175
179 public Task Synchronize()
180 {
181 this.timer?.Dispose();
182 this.timer = null;
183
184 return this.DelayedCheckSynchronization();
185 }
186
187 private void DelayedCheckSynchronization(object P)
188 {
189 Task.Run(async () =>
190 {
191 try
192 {
193 await this.DelayedCheckSynchronization();
194 }
195 catch (Exception ex)
196 {
197 Log.Exception(ex);
198 }
199 });
200 }
201
202 private Task DelayedCheckSynchronization()
203 {
204 return FilesModule.CheckSynchronization(this);
205 }
206
207 internal async void Watcher_Error(object Sender, ErrorEventArgs e)
208 {
209 try
210 {
211 await this.LogErrorAsync(e.GetException().Message);
212 }
213 catch (Exception ex)
214 {
215 Log.Exception(ex);
216 }
217 }
218
219 internal async void Watcher_Renamed(object Sender, RenamedEventArgs e)
220 {
221 try
222 {
223 await this.OnRenamed(e.OldFullPath, e.FullPath);
224 }
225 catch (Exception ex)
226 {
227 Log.Exception(ex);
228 }
229 }
230
231 internal async void Watcher_Deleted(object Sender, FileSystemEventArgs e)
232 {
233 try
234 {
235 await this.OnDeleted(e.FullPath);
236 }
237 catch (Exception ex)
238 {
239 Log.Exception(ex);
240 }
241 }
242
243 internal async void Watcher_Created(object Sender, FileSystemEventArgs e)
244 {
245 try
246 {
247 await this.OnCreated(e.FullPath);
248 }
249 catch (Exception ex)
250 {
251 Log.Exception(ex);
252 }
253 }
254
255 internal async void Watcher_Changed(object Sender, FileSystemEventArgs e)
256 {
257 try
258 {
259 if (e.ChangeType == WatcherChangeTypes.Changed)
260 await this.OnChanged(e.FullPath, null);
261 }
262 catch (Exception ex)
263 {
264 Log.Exception(ex);
265 }
266 }
267
268 internal async Task SynchFolder()
269 {
270 await this.SynchFolder(this.synchronizationOptions, this.fileFilter, null);
271 }
272
273 internal async Task SynchFolder(SynchronizationOptions Options, string Filter, SynchronizationStatistics Statistics)
274 {
275 if (Options != SynchronizationOptions.NoSynchronization)
276 {
277 Log.Informational("Starting synchronizing folder.",
278 new KeyValuePair<string, object>("Folder", this.folderPath),
279 new KeyValuePair<string, object>("Node ID", this.NodeId));
280
281 if (!(Statistics is null))
282 await Statistics.Start();
283 try
284 {
285 DirectoryInfo DirInfo = new DirectoryInfo(this.folderPath);
286 FileInfo[] Files = DirInfo.GetFiles(string.IsNullOrEmpty(Filter) ? "*.*" : this.FileFilter,
287 Options == SynchronizationOptions.IncludeSubfolders ?
288 SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
289
290 foreach (FileInfo File in Files)
291 {
292 try
293 {
294 await this.OnChanged(File.FullName, Statistics);
295 }
296 catch (Exception ex)
297 {
298 Log.Exception(ex);
299 }
300 }
301
302 Dictionary<string, Guid> ObjectIdsByPath = new Dictionary<string, Guid>();
303 LinkedList<Tuple<string, INode, INode>> ToCheck = new LinkedList<Tuple<string, INode, INode>>();
304 ToCheck.AddLast(new Tuple<string, INode, INode>(null, null, this));
305
306 while (!(ToCheck.First is null))
307 {
308 Tuple<string, INode, INode> P = ToCheck.First.Value;
309 string ParentPath = P.Item1;
310 INode Parent = P.Item2;
311 INode Node = P.Item3;
312
313 ToCheck.RemoveFirst();
314
315 if (Node is FileNode FileNode)
316 {
317 if (!File.Exists(FileNode.FolderPath) || ObjectIdsByPath.ContainsKey(FileNode.FolderPath))
318 {
319 await Parent.RemoveAsync(Node);
320 await FileNode.DestroyAsync();
321
322 if (!(Statistics is null))
323 await Statistics.FileDeleted(ParentPath, FileNode.FolderPath);
324 }
325 else
326 ObjectIdsByPath[FileNode.FolderPath] = FileNode.ObjectId;
327 }
328 else if (Node is SubFolderNode SubFolderNode)
329 {
330 if (!Directory.Exists(SubFolderNode.FolderPath) || ObjectIdsByPath.ContainsKey(SubFolderNode.FolderPath))
331 {
332 await Parent.RemoveAsync(Node);
334
335 if (!(Statistics is null))
336 await Statistics.FolderDeleted(ParentPath, SubFolderNode.FolderPath);
337 }
338 else
339 {
341
342 foreach (INode Child in await SubFolderNode.ChildNodes)
343 ToCheck.AddLast(new Tuple<string, INode, INode>(SubFolderNode.FolderPath, SubFolderNode, Child));
344 }
345 }
346 else if (Node is FolderNode FolderNode)
347 {
348 foreach (INode Child in await FolderNode.ChildNodes)
349 ToCheck.AddLast(new Tuple<string, INode, INode>(FolderNode.FolderPath, FolderNode, Child));
350 }
351 }
352 }
353 catch (Exception ex)
354 {
355 if (!(Statistics is null))
356 await Statistics.Error(ex);
357 else
358 {
359 Log.Exception(ex,
360 new KeyValuePair<string, object>("Folder", this.folderPath),
361 new KeyValuePair<string, object>("Node ID", this.NodeId));
362 }
363 }
364 finally
365 {
366 if (!(Statistics is null))
367 await Statistics.Done();
368 }
369
370 Log.Informational("Synchronization of folder complete.",
371 new KeyValuePair<string, object>("Folder", this.folderPath),
372 new KeyValuePair<string, object>("Node ID", this.NodeId));
373 }
374 }
375
376 private async Task<INode> FindNodeLocked(string Path, bool CreateIfNecessary, SynchronizationStatistics Statistics)
377 {
378 if (!Path.StartsWith(this.folderPath, StringComparison.InvariantCultureIgnoreCase))
379 return null;
380
381 Path = Path.Substring(this.folderPath.Length);
382
383 if (Path.StartsWith(directorySeparator))
384 Path = Path.Substring(1);
385
386 Dictionary<string, string> DefaultTemplates = new Dictionary<string, string>();
387 INode Parent = this;
388 string SubPath = this.folderPath;
389 string s, s2;
390 int i;
391 bool Found;
392
393 if (!(this.MetaData is null))
394 {
395 foreach (MetaDataValue Tag in this.MetaData)
396 {
397 if (Tag.Value is string s3)
398 DefaultTemplates[Tag.Name] = s3;
399 }
400 }
401
402 while (!string.IsNullOrEmpty(Path))
403 {
404 i = Path.IndexOf(System.IO.Path.DirectorySeparatorChar);
405
406 if (i < 0)
407 {
408 s = Path;
409 Path = string.Empty;
410 }
411 else
412 {
413 s = Path.Substring(0, i);
414 Path = Path.Substring(i + 1);
415 }
416
417 s2 = System.IO.Path.Combine(SubPath, s);
418 Found = false;
419
420 foreach (INode Child in await Parent.ChildNodes)
421 {
422 if (Child is SubFolderNode SubFolderNode)
423 {
424 if (string.Compare(s2, SubFolderNode.FolderPath, true) == 0)
425 {
426 if (!(SubFolderNode.MetaData is null))
427 {
428 foreach (MetaDataValue Tag in SubFolderNode.MetaData)
429 {
430 if (Tag.Value is string s3)
431 DefaultTemplates[Tag.Name] = s3;
432 }
433 }
434
435 if (!(Statistics is null))
436 await Statistics.FolderFound(SubPath, s2);
437
438 Parent = Child;
439 Found = true;
440 break;
441 }
442 }
443 else if (Child is FileNode FileNode)
444 {
445 if (string.Compare(s2, FileNode.FolderPath, true) == 0)
446 {
447 if (!string.IsNullOrEmpty(Path))
448 return null;
449
450 if (!(Statistics is null))
451 await Statistics.FileFound(SubPath, s2);
452
453 Parent = Child;
454 Found = true;
455 break;
456 }
457 }
458 }
459
460 if (!Found)
461 {
462 if (!CreateIfNecessary)
463 return null;
464
465 if (string.IsNullOrEmpty(Path) && File.Exists(s2))
466 {
467 string FileExtension = System.IO.Path.GetExtension(s2);
468 if (!string.IsNullOrEmpty(FileExtension) && FileExtension[0] == '.')
469 FileExtension = FileExtension.Substring(1);
470
471 if (!DefaultTemplates.TryGetValue(FileExtension, out string Template))
472 Template = string.Empty;
473
474 FileNode Node = new FileNode()
475 {
476 NodeId = await GetUniqueNodeId(s2),
477 FolderPath = s2,
478 ScriptNodeId = Template
479 };
480
481 await Parent.AddAsync(Node);
482
483 if (!(Statistics is null))
484 await Statistics.FileAdded(SubPath, s2);
485
486 Log.Informational("File node added.",
487 new KeyValuePair<string, object>("Folder", Node.FolderPath),
488 new KeyValuePair<string, object>("Node ID", Node.NodeId),
489 new KeyValuePair<string, object>("Script Node ID", Node.ScriptNodeId));
490
491 Parent = Node;
492 }
493 else
494 {
495 SubFolderNode Node = new SubFolderNode()
496 {
497 NodeId = await GetUniqueNodeId(s2),
498 FolderPath = s2
499 };
500
501 await Parent.AddAsync(Node);
502
503 if (!(Statistics is null))
504 await Statistics.FolderAdded(SubPath, s2);
505
506 Log.Informational("Folder node added.",
507 new KeyValuePair<string, object>("Folder", Node.FolderPath),
508 new KeyValuePair<string, object>("Node ID", Node.NodeId));
509
510 Parent = Node;
511 }
512 }
513
514 SubPath = s2;
515 }
516
517 return Parent;
518 }
519
520 internal static string GetLocalName(string Path)
521 {
522 string[] Parts = Path.Split(System.IO.Path.DirectorySeparatorChar);
523 int c = Parts.Length;
524 string s;
525
526 if (--c < 0)
527 return Path;
528
529 if (!string.IsNullOrEmpty(s = Parts[c]))
530 return s;
531
532 if (--c < 0)
533 return Path;
534
535 if (!string.IsNullOrEmpty(s = Parts[c]))
536 return s;
537 else
538 return Path;
539 }
540
541 private static readonly string directorySeparator = new string(Path.DirectorySeparatorChar, 1);
542
543 private async Task OnCreated(string Path)
544 {
545 await this.synchObj.WaitAsync();
546 try
547 {
548 INode Node = await this.FindNodeLocked(Path, true, null);
549 await this.ExecuteAssociatedScript(Node);
550 }
551 finally
552 {
553 this.synchObj.Release();
554 }
555 }
556
557 private async Task OnChanged(string Path, SynchronizationStatistics Statistics)
558 {
559 await this.synchObj.WaitAsync();
560 try
561 {
562 INode Node = await this.FindNodeLocked(Path, true, Statistics);
563 await this.ExecuteAssociatedScript(Node);
564 }
565 finally
566 {
567 this.synchObj.Release();
568 }
569 }
570
571 private async Task ExecuteAssociatedScript(INode Node)
572 {
573 if (Node is ScriptReferenceNode ScriptReferenceNode && !string.IsNullOrEmpty(ScriptReferenceNode.ScriptNodeId))
574 {
575 InternalReadoutRequest InternalReadout = new InternalReadoutRequest(Node.LogId, null,
576 SensorData.FieldType.Momentary, null, DateTime.MinValue, DateTime.MaxValue,
577 (Sender, e) =>
578 {
579 return ScriptReferenceNode.NewMomentaryValues(e.Fields);
580 },
581 async (Sender, e) =>
582 {
583 foreach (ThingError Error in e.Errors)
585 }, null);
586
587 await ScriptReferenceNode.StartReadout(InternalReadout);
588 }
589 }
590
591 private async Task OnRenamed(string OldPath, string NewPath)
592 {
593 await this.synchObj.WaitAsync();
594 try
595 {
596 INode OldNode = await this.FindNodeLocked(OldPath, false, null);
597 INode NewNode = await this.FindNodeLocked(NewPath, true, null);
598
599 if (OldNode is null || OldNode.NodeId == NewNode.NodeId)
600 return;
601
602 if (NewNode is ScriptReferenceNode NewScriptReferenceNode && OldNode is ScriptReferenceNode OldScriptReferenceNode)
603 NewScriptReferenceNode.ScriptNodeId = OldScriptReferenceNode.ScriptNodeId;
604
605 if (NewNode is VirtualNode NewVirtualNode && OldNode is VirtualNode OldVirtualNode)
606 NewVirtualNode.MetaData = OldVirtualNode.MetaData;
607
608 if (NewNode is ProvisionedMeteringNode NewProvisionedMeteringNode && OldNode is ProvisionedMeteringNode OldProvisionedMeteringNode)
609 {
610 NewProvisionedMeteringNode.OwnerAddress = OldProvisionedMeteringNode.OwnerAddress;
611 NewProvisionedMeteringNode.Public = OldProvisionedMeteringNode.Public;
612 NewProvisionedMeteringNode.Provisioned = OldProvisionedMeteringNode.Provisioned;
613 }
614
615 if (NewNode is MetaMeteringNode NewMetaMeteringNode && OldNode is MetaMeteringNode OldMetaMeteringNode)
616 {
617 NewMetaMeteringNode.Name = OldMetaMeteringNode.Name;
618 NewMetaMeteringNode.Class = OldMetaMeteringNode.Class;
619 NewMetaMeteringNode.SerialNumber = OldMetaMeteringNode.SerialNumber;
620 NewMetaMeteringNode.MeterNumber = OldMetaMeteringNode.MeterNumber;
621 NewMetaMeteringNode.MeterLocation = OldMetaMeteringNode.MeterLocation;
622 NewMetaMeteringNode.ManufacturerDomain = OldMetaMeteringNode.ManufacturerDomain;
623 NewMetaMeteringNode.Model = OldMetaMeteringNode.Model;
624 NewMetaMeteringNode.Version = OldMetaMeteringNode.Version;
625 NewMetaMeteringNode.ProductUrl = OldMetaMeteringNode.ProductUrl;
626 NewMetaMeteringNode.Country = OldMetaMeteringNode.Country;
627 NewMetaMeteringNode.Region = OldMetaMeteringNode.Region;
628 NewMetaMeteringNode.City = OldMetaMeteringNode.City;
629 NewMetaMeteringNode.Street = OldMetaMeteringNode.Street;
630 NewMetaMeteringNode.StreetNr = OldMetaMeteringNode.StreetNr;
631 NewMetaMeteringNode.Building = OldMetaMeteringNode.Building;
632 NewMetaMeteringNode.Apartment = OldMetaMeteringNode.Apartment;
633 NewMetaMeteringNode.Room = OldMetaMeteringNode.Room;
634 NewMetaMeteringNode.Latitude = OldMetaMeteringNode.Latitude;
635 NewMetaMeteringNode.Longitude = OldMetaMeteringNode.Longitude;
636 NewMetaMeteringNode.Altitude = OldMetaMeteringNode.Altitude;
637 }
638
639 await NewNode.UpdateAsync();
640
641 if (!(OldNode.Parent is null))
642 await OldNode.Parent.RemoveAsync(OldNode);
643
644 await OldNode.DestroyAsync();
645
646 Log.Informational("File Node renamed.",
647 new KeyValuePair<string, object>("Old Node ID", OldNode.NodeId),
648 new KeyValuePair<string, object>("New Node ID", NewNode.NodeId));
649 }
650 finally
651 {
652 this.synchObj.Release();
653 }
654 }
655
656 private async Task OnDeleted(string Path)
657 {
658 await this.synchObj.WaitAsync();
659 try
660 {
661 INode Node = await this.FindNodeLocked(Path, false, null);
662 if (Node is null)
663 return;
664
665 if (!(Node.Parent is null))
666 await Node.Parent.RemoveAsync(Node);
667
668 await Node.DestroyAsync();
669
670 Log.Informational("File Node deleted.",
671 new KeyValuePair<string, object>("Node ID", Node.NodeId));
672 }
673 finally
674 {
675 this.synchObj.Release();
676 }
677 }
678
682 public override Task<IEnumerable<ICommand>> Commands => this.GetCommands();
683
687 private async Task<IEnumerable<ICommand>> GetCommands()
688 {
689 List<ICommand> Commands = new List<ICommand>();
690 Commands.AddRange(await base.Commands);
691
692 Commands.Add(new SynchronizeFolder(this));
693
694 return Commands.ToArray();
695 }
696
697 }
698}
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:14
static void Exception(Exception Exception, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, params KeyValuePair< string, object >[] Tags)
Logs an exception. Event type will be determined by the severity of the exception.
Definition: Log.cs:1657
static void Informational(string Message, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, string StackTrace, params KeyValuePair< string, object >[] Tags)
Logs an informational event.
Definition: Log.cs:344
Manages a chat sensor data readout request.
Contains personal sensor data.
Definition: SensorData.cs:15
Contains information about a language.
Definition: Language.cs:17
Task< string > GetStringAsync(Type Type, int Id, string Default)
Gets the string value of a string ID. If no such string exists, a string is created with the default ...
Definition: Language.cs:209
Generates basic statistics around a folder synchronization process.
async Task FileDeleted(string Folder, string FileName)
File node deleted from topology.
async Task FileFound(string Folder, string FileName)
File found, corresponding to node in topology.
async Task FileAdded(string Folder, string FileName)
File node added to topology.
async Task FolderAdded(string Folder, string SubFolder)
Subfolder node added to topology.
async Task Error(Exception ex)
An exception has occurred during synchronization.
async Task FolderDeleted(string Folder, string SubFolder)
Subfolder node deleted from topology.
async Task FolderFound(string Folder, string SubFolder)
Subfolder found, corresponding to node in topology.
Represents a file in the file system.
Definition: FileNode.cs:13
string FolderPath
Full path to folder.
Definition: FileNode.cs:28
Module maintaining active file system watchers.
Definition: FilesModule.cs:19
Represents a file folder in the file system.
Definition: FolderNode.cs:44
override Task< IEnumerable< ICommand > > Commands
Available command objects. If no commands are available, null is returned.
Definition: FolderNode.cs:682
override async Task DestroyAsync()
Destroys the node. If it is a child to a parent node, it is removed from the parent first.
Definition: FolderNode.cs:61
override Task< bool > AcceptsParentAsync(INode Parent)
If the node accepts a presumptive parent, i.e. can be added to that parent (if that parent accepts th...
Definition: FolderNode.cs:160
override Task< bool > AcceptsChildAsync(INode Child)
If the node accepts a presumptive child, i.e. can receive as a child (if that child accepts the node ...
Definition: FolderNode.cs:148
string FolderPath
Full path to folder.
Definition: FolderNode.cs:78
override Task< string > GetTypeNameAsync(Language Language)
Gets the type name of the node.
Definition: FolderNode.cs:138
FolderNode()
Represents a file folder in the file system.
Definition: FolderNode.cs:54
Task Synchronize()
Synchronizes folder, subfolders, files and nodes.
Definition: FolderNode.cs:179
string FileFilter
File filter to monitor
Definition: FolderNode.cs:121
Represents a subfolder in the file system.
string FolderPath
Full path to folder.
Base class for metering nodes with interoperable meta-information.
virtual async Task DestroyAsync()
Destroys the node. If it is a child to a parent node, it is removed from the parent first.
virtual Task LogErrorAsync(string Body)
Logs an error message on the node.
Guid ObjectId
Object ID in persistence layer.
Definition: MeteringNode.cs:93
static async Task< string > GetUniqueNodeId(string NodeId)
Gets a Node ID, based on NodeId that is not already available in the database.
Task< IEnumerable< INode > > ChildNodes
Child nodes. If no child nodes are available, null is returned.
Class for the root node of the Metering topology.
Definition: Root.cs:10
Base class for all provisioned metering nodes.
Node referencing a script node.
string ScriptNodeId
ID of node containing script defining node.
override async Task StartReadout(ISensorReadout Request, bool DoneAfter)
Starts the readout of the sensor.
Contains information about an error on a thing
Definition: ThingError.cs:10
string ErrorMessage
Error message.
Definition: ThingError.cs:70
Class representing a meta-data value.
Virtual node, that can be used as a placeholder for services.
Definition: VirtualNode.cs:28
MetaDataValue[] MetaData
Meta-data attached to virtual node.
Definition: VirtualNode.cs:47
Interface for nodes that are published through the concentrator interface.
Definition: INode.cs:49
Task AddAsync(INode Child)
Adds a new child to the node.
Task DestroyAsync()
Destroys the node. If it is a child to a parent node, it is removed from the parent first.
Task UpdateAsync()
Updates the node (in persisted storage).
Task< IEnumerable< INode > > ChildNodes
Child nodes. If no child nodes are available, null is returned.
Definition: INode.cs:140
Task< bool > RemoveAsync(INode Child)
Removes a child from the node.
INode Parent
Parent Node, or null if a root node.
Definition: INode.cs:116
string LogId
If provided, an ID for the node, as it would appear or be used in system logs. Can be null,...
Definition: INode.cs:62
TextPosition
Where the instructions are to be place.
Definition: TextAttribute.cs:9
SynchronizationOptions
How a folder will synchronize nodes with contents of folders.
Definition: FolderNode.cs:23