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;
2using System.Collections.Generic;
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(Child is SubFolderNode || Child is FileNode);
151 }
152
158 public override Task<bool> AcceptsParentAsync(INode Parent)
159 {
160 return Task.FromResult(Parent is Root || Parent is VirtualNode);
161 }
162
163 private void CheckSynchronization()
164 {
165 this.timer?.Dispose();
166 this.timer = null;
167
168 this.timer = new Timer(this.DelayedCheckSynchronization, null, 500, Timeout.Infinite);
169 }
170
174 public Task Synchronize()
175 {
176 this.timer?.Dispose();
177 this.timer = null;
178
179 return this.DelayedCheckSynchronization();
180 }
181
182 private void DelayedCheckSynchronization(object P)
183 {
184 Task.Run(async () =>
185 {
186 try
187 {
188 await this.DelayedCheckSynchronization();
189 }
190 catch (Exception ex)
191 {
192 Log.Exception(ex);
193 }
194 });
195 }
196
197 private Task DelayedCheckSynchronization()
198 {
199 return FilesModule.CheckSynchronization(this);
200 }
201
202 internal async void Watcher_Error(object Sender, ErrorEventArgs e)
203 {
204 try
205 {
206 await this.LogErrorAsync(e.GetException().Message);
207 }
208 catch (Exception ex)
209 {
210 Log.Exception(ex);
211 }
212 }
213
214 internal async void Watcher_Renamed(object Sender, RenamedEventArgs e)
215 {
216 try
217 {
218 await this.OnRenamed(e.OldFullPath, e.FullPath);
219 }
220 catch (Exception ex)
221 {
222 Log.Exception(ex);
223 }
224 }
225
226 internal async void Watcher_Deleted(object Sender, FileSystemEventArgs e)
227 {
228 try
229 {
230 await this.OnDeleted(e.FullPath);
231 }
232 catch (Exception ex)
233 {
234 Log.Exception(ex);
235 }
236 }
237
238 internal async void Watcher_Created(object Sender, FileSystemEventArgs e)
239 {
240 try
241 {
242 await this.OnCreated(e.FullPath);
243 }
244 catch (Exception ex)
245 {
246 Log.Exception(ex);
247 }
248 }
249
250 internal async void Watcher_Changed(object Sender, FileSystemEventArgs e)
251 {
252 try
253 {
254 if (e.ChangeType == WatcherChangeTypes.Changed)
255 await this.OnChanged(e.FullPath, null);
256 }
257 catch (Exception ex)
258 {
259 Log.Exception(ex);
260 }
261 }
262
263 internal async Task SynchFolder()
264 {
265 await this.SynchFolder(this.synchronizationOptions, this.fileFilter, null);
266 }
267
268 internal async Task SynchFolder(SynchronizationOptions Options, string Filter, SynchronizationStatistics Statistics)
269 {
270 if (Options != SynchronizationOptions.NoSynchronization)
271 {
272 Log.Informational("Starting synchronizing folder.",
273 new KeyValuePair<string, object>("Folder", this.folderPath),
274 new KeyValuePair<string, object>("Node ID", this.NodeId));
275
276 if (!(Statistics is null))
277 await Statistics.Start();
278 try
279 {
280 DirectoryInfo DirInfo = new DirectoryInfo(this.folderPath);
281 FileInfo[] Files = DirInfo.GetFiles(string.IsNullOrEmpty(Filter) ? "*.*" : this.FileFilter,
282 Options == SynchronizationOptions.IncludeSubfolders ?
283 SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
284
285 foreach (FileInfo File in Files)
286 {
287 try
288 {
289 await this.OnChanged(File.FullName, Statistics);
290 }
291 catch (Exception ex)
292 {
293 Log.Exception(ex);
294 }
295 }
296
297 Dictionary<string, Guid> ObjectIdsByPath = new Dictionary<string, Guid>();
298 LinkedList<Tuple<string, INode, INode>> ToCheck = new LinkedList<Tuple<string, INode, INode>>();
299 ToCheck.AddLast(new Tuple<string, INode, INode>(null, null, this));
300
301 while (!(ToCheck.First is null))
302 {
303 Tuple<string, INode, INode> P = ToCheck.First.Value;
304 string ParentPath = P.Item1;
305 INode Parent = P.Item2;
306 INode Node = P.Item3;
307
308 ToCheck.RemoveFirst();
309
310 if (Node is FileNode FileNode)
311 {
312 if (!File.Exists(FileNode.FolderPath) || ObjectIdsByPath.ContainsKey(FileNode.FolderPath))
313 {
314 await Parent.RemoveAsync(Node);
315 await FileNode.DestroyAsync();
316
317 if (!(Statistics is null))
318 await Statistics.FileDeleted(ParentPath, FileNode.FolderPath);
319 }
320 else
321 ObjectIdsByPath[FileNode.FolderPath] = FileNode.ObjectId;
322 }
323 else if (Node is SubFolderNode SubFolderNode)
324 {
325 if (!Directory.Exists(SubFolderNode.FolderPath) || ObjectIdsByPath.ContainsKey(SubFolderNode.FolderPath))
326 {
327 await Parent.RemoveAsync(Node);
329
330 if (!(Statistics is null))
331 await Statistics.FolderDeleted(ParentPath, SubFolderNode.FolderPath);
332 }
333 else
334 {
336
337 foreach (INode Child in await SubFolderNode.ChildNodes)
338 ToCheck.AddLast(new Tuple<string, INode, INode>(SubFolderNode.FolderPath, SubFolderNode, Child));
339 }
340 }
341 else if (Node is FolderNode FolderNode)
342 {
343 foreach (INode Child in await FolderNode.ChildNodes)
344 ToCheck.AddLast(new Tuple<string, INode, INode>(FolderNode.FolderPath, FolderNode, Child));
345 }
346 }
347 }
348 catch (Exception ex)
349 {
350 if (!(Statistics is null))
351 await Statistics.Error(ex);
352 else
353 {
354 Log.Exception(ex,
355 new KeyValuePair<string, object>("Folder", this.folderPath),
356 new KeyValuePair<string, object>("Node ID", this.NodeId));
357 }
358 }
359 finally
360 {
361 if (!(Statistics is null))
362 await Statistics.Done();
363 }
364
365 Log.Informational("Synchronization of folder complete.",
366 new KeyValuePair<string, object>("Folder", this.folderPath),
367 new KeyValuePair<string, object>("Node ID", this.NodeId));
368 }
369 }
370
371 private async Task<INode> FindNodeLocked(string Path, bool CreateIfNecessary, SynchronizationStatistics Statistics)
372 {
373 if (!Path.StartsWith(this.folderPath, StringComparison.InvariantCultureIgnoreCase))
374 return null;
375
376 Path = Path.Substring(this.folderPath.Length);
377
378 if (Path.StartsWith(directorySeparator))
379 Path = Path.Substring(1);
380
381 Dictionary<string, string> DefaultTemplates = new Dictionary<string, string>();
382 INode Parent = this;
383 string SubPath = this.folderPath;
384 string s, s2;
385 int i;
386 bool Found;
387
388 if (!(this.MetaData is null))
389 {
390 foreach (MetaDataValue Tag in this.MetaData)
391 {
392 if (Tag.Value is string s3)
393 DefaultTemplates[Tag.Name] = s3;
394 }
395 }
396
397 while (!string.IsNullOrEmpty(Path))
398 {
399 i = Path.IndexOf(System.IO.Path.DirectorySeparatorChar);
400
401 if (i < 0)
402 {
403 s = Path;
404 Path = string.Empty;
405 }
406 else
407 {
408 s = Path.Substring(0, i);
409 Path = Path.Substring(i + 1);
410 }
411
412 s2 = System.IO.Path.Combine(SubPath, s);
413 Found = false;
414
415 foreach (INode Child in await Parent.ChildNodes)
416 {
417 if (Child is SubFolderNode SubFolderNode)
418 {
419 if (string.Compare(s2, SubFolderNode.FolderPath, true) == 0)
420 {
421 if (!(SubFolderNode.MetaData is null))
422 {
423 foreach (MetaDataValue Tag in SubFolderNode.MetaData)
424 {
425 if (Tag.Value is string s3)
426 DefaultTemplates[Tag.Name] = s3;
427 }
428 }
429
430 if (!(Statistics is null))
431 await Statistics.FolderFound(SubPath, s2);
432
433 Parent = Child;
434 Found = true;
435 break;
436 }
437 }
438 else if (Child is FileNode FileNode)
439 {
440 if (string.Compare(s2, FileNode.FolderPath, true) == 0)
441 {
442 if (!string.IsNullOrEmpty(Path))
443 return null;
444
445 if (!(Statistics is null))
446 await Statistics.FileFound(SubPath, s2);
447
448 Parent = Child;
449 Found = true;
450 break;
451 }
452 }
453 }
454
455 if (!Found)
456 {
457 if (!CreateIfNecessary)
458 return null;
459
460 if (string.IsNullOrEmpty(Path) && File.Exists(s2))
461 {
462 string FileExtension = System.IO.Path.GetExtension(s2);
463 if (!string.IsNullOrEmpty(FileExtension) && FileExtension[0] == '.')
464 FileExtension = FileExtension.Substring(1);
465
466 if (!DefaultTemplates.TryGetValue(FileExtension, out string Template))
467 Template = string.Empty;
468
469 FileNode Node = new FileNode()
470 {
471 NodeId = await GetUniqueNodeId(s2),
472 FolderPath = s2,
473 ScriptNodeId = Template
474 };
475
476 await Parent.AddAsync(Node);
477
478 if (!(Statistics is null))
479 await Statistics.FileAdded(SubPath, s2);
480
481 Log.Informational("File node added.",
482 new KeyValuePair<string, object>("Folder", Node.FolderPath),
483 new KeyValuePair<string, object>("Node ID", Node.NodeId),
484 new KeyValuePair<string, object>("Script Node ID", Node.ScriptNodeId));
485
486 Parent = Node;
487 }
488 else
489 {
490 SubFolderNode Node = new SubFolderNode()
491 {
492 NodeId = await GetUniqueNodeId(s2),
493 FolderPath = s2
494 };
495
496 await Parent.AddAsync(Node);
497
498 if (!(Statistics is null))
499 await Statistics.FolderAdded(SubPath, s2);
500
501 Log.Informational("Folder node added.",
502 new KeyValuePair<string, object>("Folder", Node.FolderPath),
503 new KeyValuePair<string, object>("Node ID", Node.NodeId));
504
505 Parent = Node;
506 }
507 }
508
509 SubPath = s2;
510 }
511
512 return Parent;
513 }
514
515 internal static string GetLocalName(string Path)
516 {
517 string[] Parts = Path.Split(System.IO.Path.DirectorySeparatorChar);
518 int c = Parts.Length;
519 string s;
520
521 if (--c < 0)
522 return Path;
523
524 if (!string.IsNullOrEmpty(s = Parts[c]))
525 return s;
526
527 if (--c < 0)
528 return Path;
529
530 if (!string.IsNullOrEmpty(s = Parts[c]))
531 return s;
532 else
533 return Path;
534 }
535
536 private static readonly string directorySeparator = new string(Path.DirectorySeparatorChar, 1);
537
538 private async Task OnCreated(string Path)
539 {
540 await this.synchObj.WaitAsync();
541 try
542 {
543 INode Node = await this.FindNodeLocked(Path, true, null);
544 await this.ExecuteAssociatedScript(Node);
545 }
546 finally
547 {
548 this.synchObj.Release();
549 }
550 }
551
552 private async Task OnChanged(string Path, SynchronizationStatistics Statistics)
553 {
554 await this.synchObj.WaitAsync();
555 try
556 {
557 INode Node = await this.FindNodeLocked(Path, true, Statistics);
558 await this.ExecuteAssociatedScript(Node);
559 }
560 finally
561 {
562 this.synchObj.Release();
563 }
564 }
565
566 private async Task ExecuteAssociatedScript(INode Node)
567 {
568 if (Node is ScriptReferenceNode ScriptReferenceNode && !string.IsNullOrEmpty(ScriptReferenceNode.ScriptNodeId))
569 {
570 InternalReadoutRequest InternalReadout = new InternalReadoutRequest(Node.LogId, null,
571 SensorData.FieldType.Momentary, null, DateTime.MinValue, DateTime.MaxValue,
572 (Sender, e) =>
573 {
574 ScriptReferenceNode.NewMomentaryValues(e.Fields);
575 return Task.CompletedTask;
576 },
577 async (Sender, e) =>
578 {
579 foreach (ThingError Error in e.Errors)
581 }, null);
582
583 await ScriptReferenceNode.StartReadout(InternalReadout);
584 }
585 }
586
587 private async Task OnRenamed(string OldPath, string NewPath)
588 {
589 await this.synchObj.WaitAsync();
590 try
591 {
592 INode OldNode = await this.FindNodeLocked(OldPath, false, null);
593 INode NewNode = await this.FindNodeLocked(NewPath, true, null);
594
595 if (OldNode is null || OldNode.NodeId == NewNode.NodeId)
596 return;
597
598 if (NewNode is ScriptReferenceNode NewScriptReferenceNode && OldNode is ScriptReferenceNode OldScriptReferenceNode)
599 NewScriptReferenceNode.ScriptNodeId = OldScriptReferenceNode.ScriptNodeId;
600
601 if (NewNode is VirtualNode NewVirtualNode && OldNode is VirtualNode OldVirtualNode)
602 NewVirtualNode.MetaData = OldVirtualNode.MetaData;
603
604 if (NewNode is ProvisionedMeteringNode NewProvisionedMeteringNode && OldNode is ProvisionedMeteringNode OldProvisionedMeteringNode)
605 {
606 NewProvisionedMeteringNode.OwnerAddress = OldProvisionedMeteringNode.OwnerAddress;
607 NewProvisionedMeteringNode.Public = OldProvisionedMeteringNode.Public;
608 NewProvisionedMeteringNode.Provisioned = OldProvisionedMeteringNode.Provisioned;
609 }
610
611 if (NewNode is MetaMeteringNode NewMetaMeteringNode && OldNode is MetaMeteringNode OldMetaMeteringNode)
612 {
613 NewMetaMeteringNode.Name = OldMetaMeteringNode.Name;
614 NewMetaMeteringNode.Class = OldMetaMeteringNode.Class;
615 NewMetaMeteringNode.SerialNumber = OldMetaMeteringNode.SerialNumber;
616 NewMetaMeteringNode.MeterNumber = OldMetaMeteringNode.MeterNumber;
617 NewMetaMeteringNode.MeterLocation = OldMetaMeteringNode.MeterLocation;
618 NewMetaMeteringNode.ManufacturerDomain = OldMetaMeteringNode.ManufacturerDomain;
619 NewMetaMeteringNode.Model = OldMetaMeteringNode.Model;
620 NewMetaMeteringNode.Version = OldMetaMeteringNode.Version;
621 NewMetaMeteringNode.ProductUrl = OldMetaMeteringNode.ProductUrl;
622 NewMetaMeteringNode.Country = OldMetaMeteringNode.Country;
623 NewMetaMeteringNode.Region = OldMetaMeteringNode.Region;
624 NewMetaMeteringNode.City = OldMetaMeteringNode.City;
625 NewMetaMeteringNode.Street = OldMetaMeteringNode.Street;
626 NewMetaMeteringNode.StreetNr = OldMetaMeteringNode.StreetNr;
627 NewMetaMeteringNode.Building = OldMetaMeteringNode.Building;
628 NewMetaMeteringNode.Apartment = OldMetaMeteringNode.Apartment;
629 NewMetaMeteringNode.Room = OldMetaMeteringNode.Room;
630 NewMetaMeteringNode.Latitude = OldMetaMeteringNode.Latitude;
631 NewMetaMeteringNode.Longitude = OldMetaMeteringNode.Longitude;
632 NewMetaMeteringNode.Altitude = OldMetaMeteringNode.Altitude;
633 }
634
635 await NewNode.UpdateAsync();
636
637 if (!(OldNode.Parent is null))
638 await OldNode.Parent.RemoveAsync(OldNode);
639
640 await OldNode.DestroyAsync();
641
642 Log.Informational("File Node renamed.",
643 new KeyValuePair<string, object>("Old Node ID", OldNode.NodeId),
644 new KeyValuePair<string, object>("New Node ID", NewNode.NodeId));
645 }
646 finally
647 {
648 this.synchObj.Release();
649 }
650 }
651
652 private async Task OnDeleted(string Path)
653 {
654 await this.synchObj.WaitAsync();
655 try
656 {
657 INode Node = await this.FindNodeLocked(Path, false, null);
658 if (Node is null)
659 return;
660
661 if (!(Node.Parent is null))
662 await Node.Parent.RemoveAsync(Node);
663
664 await Node.DestroyAsync();
665
666 Log.Informational("File Node deleted.",
667 new KeyValuePair<string, object>("Node ID", Node.NodeId));
668 }
669 finally
670 {
671 this.synchObj.Release();
672 }
673 }
674
678 public override Task<IEnumerable<ICommand>> Commands => this.GetCommands();
679
683 private async Task<IEnumerable<ICommand>> GetCommands()
684 {
685 List<ICommand> Commands = new List<ICommand>();
686 Commands.AddRange(await base.Commands);
687
688 Commands.Add(new SynchronizeFolder(this));
689
690 return Commands.ToArray();
691 }
692
693 }
694}
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:13
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:1647
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:334
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:18
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:678
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:158
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:174
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:90
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:11
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