GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
GPALFile.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.IO;
20using System.Linq;
21using System.Net.Http;
22using System.Numerics;
23using System.Text;
24using System.Threading.Tasks;
25using System.Windows;
26using static GenerallyPositive.Enums;
27
28
29namespace GenerallyPositive
30{
35 public class GPALFile : IGPALFile, IGPALFileInternal
36 {
37
42 GPALFileSettings IGPALFileInternal.FileSettings { get; set; }
43
48 public GPALFile ToGPALObject()
49 {
50 return this;
51 }
52
56 internal GPALFile()
57 {
58 ((IGPALFileInternal)this).FileSettings = new GPALFileSettings();
59 ((IGPALFileInternal)this).FileDictionary = new List<Dictionary<object, dynamic>>();
60 }
61
67 internal GPALFile(string fileName)
68 {
69 ((IGPALFileInternal)this).FileSettings = new GPALFileSettings();
70 ((IGPALFileInternal)this).FileSettings.Filenames.Add(fileName);
71 ((IGPALFileInternal)this).FileDictionary = new List<Dictionary<object, dynamic>>();
72 }
73 #region <Fluent Interface>
80 public IAllowFileSettings WithOverwriteFile(bool overwriteFile = false)
81 {
82 ((IGPALFileInternal)this).FileSettings.OverwriteFile.Add(overwriteFile);
83 return this;
84 }
85
92 public IAllowFileSettings WithFileSortOrder(Enums.FileSortOrder fileSortOrder)
93 {
94 ((IGPALFileInternal)this).FileSettings.FileSortOrder = fileSortOrder;
95 return this;
96 }
97
104 public IAllowFileSettings WithNextFilePattern(Enums.NextFilePattern nextFilePattern = Enums.NextFilePattern.CounterPadded)
105 {
106 ((IGPALFileInternal)this).FileSettings.NextFilePattern = nextFilePattern;
107 return this;
108 }
109
114 public IAllowFileSettings WithDelimiter(char delimiter)
115 {
116 ((IGPALFileInternal)this).FileSettings.Delimiter.Add(delimiter);
117 return this;
118 }
119
126 {
127 ((IGPALFileInternal)this).FileSettings.FieldsEnclosedInQuotes.Add(fieldsInQuotes);
128 return this;
129 }
130
135 public IAllowFileSettings WithFirstLineIsColumnNames(bool firstLineIsColumnNames)
136 {
137 ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames.Add(firstLineIsColumnNames);
138 return this;
139 }
140
145 public IAllowFileSettings WithIgnoreFirstLineColumnNames(bool ignoreFirstLine = true)
146 {
147 ((IGPALFileInternal)this).FileSettings.IgnoreFirstLineColumnNames.Add(ignoreFirstLine);
148 return this;
149 }
150
157 {
158 ((IGPALFileInternal)this).FileSettings.UseOneHeaderForAllFiles = useHeaderForAllFiles;
159 return this;
160 }
161
181 {
182 ((IGPALFileInternal)this).FileSettings.Browser = browser;
183 return this;
184 }
185
202 public IAllowFileSettings WithFileName(string fileName)
203 {
204 fileName = Environment.ExpandEnvironmentVariables(fileName);
205
206 // before the wildcard test, since a url's query string carries a "?" that would otherwise be read as one
207 if (true == FileHelper.IsWebAddress(fileName))
208 {
209 Browser.IBrowser browser = ((IGPALFileInternal)this).FileSettings.Browser;
210 string name = Path.GetFileName(new Uri(fileName).LocalPath);
211 string localPath = Path.Combine(GPAL.TempDirectory(), true == string.IsNullOrWhiteSpace(name) ? "download" : name);
212
213 ((IGPALFileInternal)this).FileSettings.SourceUrl = fileName;
214
215 // supplying a browser is how a workflow says this file is behind something our own connection
216 // cannot get past, so the browser gets it: fetched from inside the page, and downloaded in a tab
217 // only if the page cannot hand it over.
218 // Without a browser there is only our own connection, which works for anything public
219 if (null != browser)
220 FileHelper.FetchOrDownloadInTab(fileName, localPath, browser);
221 else
222 FileHelper.DownloadToFile(null, localPath, fileName);
223
224 fileName = localPath;
225 }
226
227 if (true == fileName.Contains('*') || true == fileName.Contains('?'))
228 {
229 string directory = Path.GetDirectoryName(fileName);
230 string filenamePart = Path.GetFileName(fileName);
231 string[] matches = Directory.GetFiles(false == string.IsNullOrEmpty(directory) ? directory : @".\", filenamePart);
232
233 if (0 == matches.Length)
234 {
235 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING, $"No files matched the wildcard pattern [{fileName}].", this, Enums.GPALObjectType.GPALFile);
236 }
237 else
238 {
239 IEnumerable<string> sortedMatches;
240
241 switch (((IGPALFileInternal)this).FileSettings.FileSortOrder)
242 {
243 case Enums.FileSortOrder.NameDescending:
244 sortedMatches = matches.OrderByDescending(m => m, StringComparer.OrdinalIgnoreCase);
245 break;
246 case Enums.FileSortOrder.NaturalAscending:
247 sortedMatches = matches.OrderBy(m => m, NaturalFilenameComparer.Instance);
248 break;
249 case Enums.FileSortOrder.NaturalDescending:
250 sortedMatches = matches.OrderByDescending(m => m, NaturalFilenameComparer.Instance);
251 break;
252 case Enums.FileSortOrder.DateModifiedAscending:
253 sortedMatches = matches.OrderBy(m => File.GetLastWriteTimeUtc(m));
254 break;
255 case Enums.FileSortOrder.DateModifiedDescending:
256 sortedMatches = matches.OrderByDescending(m => File.GetLastWriteTimeUtc(m));
257 break;
258 case Enums.FileSortOrder.DateCreatedAscending:
259 sortedMatches = matches.OrderBy(m => File.GetCreationTimeUtc(m));
260 break;
261 case Enums.FileSortOrder.DateCreatedDescending:
262 sortedMatches = matches.OrderByDescending(m => File.GetCreationTimeUtc(m));
263 break;
264 case Enums.FileSortOrder.NameAscending:
265 default:
266 sortedMatches = matches.OrderBy(m => m, StringComparer.OrdinalIgnoreCase);
267 break;
268 }
269
270 foreach (string match in sortedMatches)
271 {
272 ((IGPALFileInternal)this).FileSettings.Filenames.Add(match);
273 ((IGPALFileInternal)this).FileSettings.FileCount++;
274 }
275 }
276 }
277 else
278 {
279 ((IGPALFileInternal)this).FileSettings.Filenames.Add(fileName);
280 ((IGPALFileInternal)this).FileSettings.FileCount++;
281 }
282
283 return this;
284 }
285
292 public IAllowFileSettings WithColumnName(string columnName)
293 {
294 if (((IGPALFileInternal)this).FileSettings.FileCount > ((IGPALFileInternal)this).FileSettings.ColumnList.Count())
295 ((IGPALFileInternal)this).FileSettings.ColumnList.AddRow(new List<string>());
296
297 ((IGPALFileInternal)this).FileSettings.ColumnList[((IGPALFileInternal)this).FileSettings.FileCount - 1].Add(columnName);
298
299 if (((IGPALFileInternal)this).FileSettings.FileCount > ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames.Count)
300 ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames.Add(true);
301 return this;
302 }
303
311 public IAllowFileSettings WithColumnNames(string columnNames)
312 {
313 if (((IGPALFileInternal)this).FileSettings.FileCount > ((IGPALFileInternal)this).FileSettings.ColumnList.Count())
314 ((IGPALFileInternal)this).FileSettings.ColumnList.AddRow(new List<string>());
315 else
316 // we already have columns from .WithColumnName, add a new list for our entries
317 {
318 string oldColumns = string.Join(", ", ((IGPALFileInternal)this).FileSettings.ColumnList[((IGPALFileInternal)this).FileSettings.FileCount - 1]);
319 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING, $"Overwriting existing column names [{oldColumns}] with new names [{columnNames}]");
320 ((IGPALFileInternal)this).FileSettings.ColumnList.Clear();
321 }
322
323 foreach (string columnName in columnNames.Split(','))
324 ((IGPALFileInternal)this).FileSettings.ColumnList[((IGPALFileInternal)this).FileSettings.FileCount - 1].Add(columnName.Trim());
325
326 // alway ensure we have a new empty list waiting to add columns too.
327
328 if (((IGPALFileInternal)this).FileSettings.FileCount > ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames.Count)
329 ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames.Add(true);
330
331 return this;
332 }
333
341 public IAllowFileSettings WithColumnNames(string[] columnNames)
342 {
343 // we already have columns from .WithCOlumnName, add a new list for our entries
344 if (((IGPALFileInternal)this).FileSettings.FileCount > ((IGPALFileInternal)this).FileSettings.ColumnList.Count())
345 ((IGPALFileInternal)this).FileSettings.ColumnList.AddRow(new List<string>());
346
347 char delim;
348 if (0 < ((IGPALFileInternal)this).FileSettings.Delimiter.Count && true == ((IGPALFileInternal)this).FileSettings.Delimiter[((IGPALFileInternal)this).FileSettings.FileCount].HasValue)
349 delim = ((IGPALFileInternal)this).FileSettings.Delimiter[((IGPALFileInternal)this).FileSettings.FileCount].Value;
350 else
351 delim = ',';
352
353 foreach (string columnName in columnNames)
354 ((IGPALFileInternal)this).FileSettings.ColumnList[((IGPALFileInternal)this).FileSettings.FileCount - 1].Add(columnName);
355
356 if (((IGPALFileInternal)this).FileSettings.FileCount > ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames.Count)
357 ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames.Add(true);
358
359 return this;
360 }
361 #endregion <Fluent Interface>
362
363 #region <Getters/Setters>
368 public string First
369 {
370 get
371 {
372 if (0 < ((IGPALFileInternal)this).FileSettings.FileCount)
373 {
374 ((IGPALFileInternal)this).FileSettings.NextFileCount = 0;
375 return ((IGPALFileInternal)this).FileSettings.Filenames[0];
376 }
377 else
378 return null;
379 }
380 }
381
385 public string Last
386 {
387 get
388 {
389 if (0 < ((IGPALFileInternal)this).FileSettings.FileCount)
390 {
391 ((IGPALFileInternal)this).FileSettings.NextFileCount = ((IGPALFileInternal)this).FileSettings.Filenames.Count - 1;
392 return ((IGPALFileInternal)this).FileSettings.Filenames[((IGPALFileInternal)this).FileSettings.Filenames.Count - 1];
393 }
394 else
395 return null;
396 }
397 }
398
406 public GPALFile Next
407 {
408 get
409 {
410 string fullPath = null;
411 string newFilename = null;
412 Enums.NextFilePattern nextPattern = ((IGPALFileInternal)this).FileSettings.NextFilePattern;
413 int patternCounter = ((IGPALFileInternal)this).FileSettings.NextPatternCounter;
414
415 if (0 == ((IGPALFileInternal)this).FileSettings.OverwriteFile.Count) // default to not overwrite since user did not specify but called next
416 {
417 if (((IGPALFileInternal)this).FileSettings.NextFileCount < ((IGPALFileInternal)this).FileSettings.FileCount) // otherwise get the next filename in the list
418 {
419 fullPath = FileHelper.CleanUpFileDestination(((IGPALFileInternal)this).FileSettings.Filenames[((IGPALFileInternal)this).FileSettings.NextFileCount], out newFilename, DeleteFileBeforeDownload: false, nextPattern, ref patternCounter);
420 ((IGPALFileInternal)this).FileSettings.NextPatternCounter = patternCounter;
421 // record it whether or not the name needed uniquifying. ReturnFilenames is the list of files
422 // we wrote, and a name that came back unchanged still gets written
423 ((IGPALFileInternal)this).FileSettings.ReturnFilenames.Add(fullPath);
424 ((IGPALFileInternal)this).FileSettings.NextFileCount++;
425 return fullPath;
426 }
427 else // we are out of user defined files but they keep calling Next past user supplied filenames, so generate a new filename using the last user supplied entry as a pattern, can call next to keep getting a new filename
428 {
429 fullPath = FileHelper.CleanUpFileDestination(((IGPALFileInternal)this).FileSettings.Filenames[((IGPALFileInternal)this).FileSettings.NextFileCount-1], out newFilename, DeleteFileBeforeDownload: false, nextPattern, ref patternCounter);
430 ((IGPALFileInternal)this).FileSettings.NextPatternCounter = patternCounter;
431 ((IGPALFileInternal)this).FileSettings.ReturnFilenames.Add(fullPath);
432 //((IGPALFileInternal)this).FileSettings.NextFileCount++;
433 return fullPath;
434 }
435 }
436 // do the same but respect the overwrite flag settings
437 else if (((IGPALFileInternal)this).FileSettings.NextFileCount < ((IGPALFileInternal)this).FileSettings.OverwriteFile.Count)
438 {
439 if (true == ((IGPALFileInternal)this).FileSettings.OverwriteFile[((IGPALFileInternal)this).FileSettings.NextFileCount]) // file flagged to overwrite - always return the same filename
440 {
441 fullPath = FileHelper.CleanUpFileDestination(((IGPALFileInternal)this).FileSettings.Filenames[((IGPALFileInternal)this).FileSettings.NextFileCount++], out newFilename, DeleteFileBeforeDownload: true);
442 ((IGPALFileInternal)this).FileSettings.ReturnFilenames.Add(fullPath);
443 return fullPath;
444 }
445 // a remaining user-supplied filename takes priority over generating a new one
446 else if (((IGPALFileInternal)this).FileSettings.NextFileCount < ((IGPALFileInternal)this).FileSettings.FileCount)
447 {
448 fullPath = FileHelper.CleanUpFileDestination(((IGPALFileInternal)this).FileSettings.Filenames[((IGPALFileInternal)this).FileSettings.NextFileCount], out newFilename, DeleteFileBeforeDownload: false, nextPattern, ref patternCounter);
449 ((IGPALFileInternal)this).FileSettings.NextPatternCounter = patternCounter;
450 // record it whether or not the name needed uniquifying. ReturnFilenames is the list of files
451 // we wrote, and a name that came back unchanged still gets written
452 ((IGPALFileInternal)this).FileSettings.ReturnFilenames.Add(fullPath);
453 ((IGPALFileInternal)this).FileSettings.NextFileCount++;
454 return fullPath;
455 }
456 }
457
458 // default to generating a new filename if we fall thru. the pattern is the last filename the
459 // workflow supplied once the cursor is past them, since Filenames only ever holds those now and
460 // deriving from a generated name is what produced base_0001_0003 style names
461 int patternIdx = Math.Min(((IGPALFileInternal)this).FileSettings.NextFileCount, ((IGPALFileInternal)this).FileSettings.FileCount - 1);
462
463 fullPath = FileHelper.CleanUpFileDestination(((IGPALFileInternal)this).FileSettings.Filenames[patternIdx], out newFilename, DeleteFileBeforeDownload: false, nextPattern, ref patternCounter);
464 ((IGPALFileInternal)this).FileSettings.NextPatternCounter = patternCounter;
465 ((IGPALFileInternal)this).FileSettings.ReturnFilenames.Add(fullPath);
466 //((IGPALFileInternal)this).FileSettings.NextFileCount++;
467 return fullPath;
468 }
469 }
470
473 public string Filename
474 {
475 get
476 {
477 return ((IGPALFileInternal)this).FileSettings.Filenames[0];
478 }
479 }
480
484 public List<string> DirectoryParts
485 {
486 get
487 {
488 return ((IGPALFileInternal)this).FileSettings.Filenames.Select(filename => Path.GetDirectoryName(filename)).ToList();
489 }
490 }
491
494 public List<string> FileParts
495 {
496 get
497 {
498 return ((IGPALFileInternal)this).FileSettings.Filenames.Select(filename => Path.GetFileName(filename)).ToList();
499 }
500 }
501
505 public string DirectoryPart
506 {
507 get
508 {
509 return Path.GetDirectoryName(((IGPALFileInternal)this).FileSettings.Filenames[0]);
510 }
511 }
512
525 public string FilePart
526 {
527 get
528 {
529 return Path.GetFileName(((IGPALFileInternal)this).FileSettings.Filenames[0]);
530 }
531 }
532
535 public List<string> Filenames
536 {
537 get
538 {
539 return ((IGPALFileInternal)this).FileSettings.Filenames;
540 }
541 }
542
545 public List<string> ReturnFilenames
546 {
547 get
548 {
549 return ((IGPALFileInternal)this).FileSettings.ReturnFilenames;
550 }
551 }
552
557 public string SourceUrl
558 {
559 get
560 {
561 return ((IGPALFileInternal)this).FileSettings.SourceUrl;
562 }
563 }
564
569 List<IGPALGrid<string>> IGPALFileInternal.TokenList
570 {
571 get
572 {
573 return ((IGPALFileInternal)this).FileSettings.TokenList;
574 }
575 set
576 {
577 ((IGPALFileInternal)this).FileSettings.TokenList = value;
578 }
579 }
583 List<string> IGPALFileInternal.FileData
584 {
585 get
586 {
587 return ((IGPALFileInternal)this).FileSettings.FileData;
588 }
589 set
590 {
591 ((IGPALFileInternal)this).FileSettings.FileData = value;
592 }
593 }
597 List<char?> IGPALFileInternal.Delimiter
598 {
599 get
600 {
601 return ((IGPALFileInternal)this).FileSettings.Delimiter;
602 }
603 set
604 {
605 ((IGPALFileInternal)this).FileSettings.Delimiter = value;
606 }
607 }
612 List<bool> IGPALFileInternal.FieldsEnclosedInQuotes
613 {
614 get
615 {
616 return ((IGPALFileInternal)this).FileSettings.FieldsEnclosedInQuotes;
617 }
618 set
619 {
620 ((IGPALFileInternal)this).FileSettings.FieldsEnclosedInQuotes = value;
621 }
622 }
627 List<bool> IGPALFileInternal.FirstLineIsColumnNames
628 {
629 get
630 {
631 return ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames;
632 }
633 set
634 {
635 ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames = value;
636 }
637 }
642 List<bool> IGPALFileInternal.IgnoreFirstLineColumnNames
643 {
644 get
645 {
646 return ((IGPALFileInternal)this).FileSettings.IgnoreFirstLineColumnNames;
647 }
648 set
649 {
650 ((IGPALFileInternal)this).FileSettings.IgnoreFirstLineColumnNames = value;
651 }
652 }
657 bool IGPALFileInternal.UseOneHeaderForAllFiles
658 {
659 get
660 {
661 return ((IGPALFileInternal)this).FileSettings.UseOneHeaderForAllFiles;
662 }
663 set
664 {
665 ((IGPALFileInternal)this).FileSettings.UseOneHeaderForAllFiles = value;
666 }
667 }
671 List<Dictionary<object, dynamic>> IGPALFileInternal.FileDictionary
672 {
673 get
674 {
675 return ((IGPALFileInternal)this).FileSettings.FileDictionary;
676 }
677 set
678 {
679 ((IGPALFileInternal)this).FileSettings.FileDictionary = value;
680 }
681 }
685 List<IGPALGrid<string>> IGPALFileInternal.FileGrid
686 {
687 get
688 {
689 return ((IGPALFileInternal)this).FileSettings.FileGrid;
690 }
691 set
692 {
693 ((IGPALFileInternal)this).FileSettings.FileGrid = value;
694 }
695 }
700 bool IGPALFileInternal.AlreadyTokenized
701 {
702 get
703 {
704 return ((IGPALFileInternal)this).FileSettings.AlreadyTokenized;
705 }
706
707 set
708 {
709 ((IGPALFileInternal)this).FileSettings.AlreadyTokenized = value;
710 }
711 }
715 public int Count
716 {
717 get => Filenames.Count;
718 }
719 #endregion <Getters/Setters>
720 #region Helpers
729 public void CopyTo(string destination)
730 {
731 string myDestination = destination;
732 bool isDir = IsDirectoryOnly(destination);
733
734 foreach (string filename in ((IGPALFileInternal)this).FileSettings.Filenames)
735 {
736 try
737 {
738 string directory = Path.GetDirectoryName(filename);
739 string filenamepart = Path.GetFileName(filename);
740
741 foreach (string filename2 in Directory.GetFiles(false == string.IsNullOrEmpty(directory) ? directory : @".\", filenamepart))
742 {
743 if (true == isDir)
744 myDestination = Path.Combine(destination, Path.GetFileName(filename2));
745 File.Copy(filename2, myDestination);
746 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{filename2}] copied to [{destination}].", this, Enums.GPALObjectType.GPALFile);
747 }
748 }
749 catch (Exception ex)
750 {
751 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Unable to copy [{filename}] to [{destination}].", this, Enums.GPALObjectType.GPALFile, ex);
752 }
753 }
754 }
755
763 public void MoveTo(string destination)
764 {
765 string myDestination = destination;
766 bool isDir = IsDirectoryOnly(destination);
767
768 foreach (string filename in ((IGPALFileInternal)this).FileSettings.Filenames)
769 {
770 try
771 {
772 string directory = Path.GetDirectoryName(filename);
773 string filenamepart = Path.GetFileName(filename);
774
775 foreach (string filename2 in Directory.GetFiles(false == string.IsNullOrEmpty(directory) ? directory : @".\", filenamepart))
776 {
777 if (true == isDir)
778 myDestination = Path.Combine(destination, Path.GetFileName(filename2));
779 File.Move(filename2, myDestination);
780 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{filename2}] moved to [{destination}].", this, Enums.GPALObjectType.GPALFile);
781 }
782 }
783 catch (Exception ex)
784 {
785 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Unable to move [{filename}] to [{destination}].", this, Enums.GPALObjectType.GPALFile, ex);
786 }
787 }
788 }
789
795 public void Delete(string destination)
796 {
797 string myDestination = destination;
798 bool isDir = IsDirectoryOnly(destination);
799
800 try
801 {
802 if (true == isDir)
803 foreach (string filename in Directory.GetFiles(destination))
804 {
805 myDestination = Path.Combine(destination, filename);
806 File.Delete(Path.Combine(myDestination, filename));
807 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{myDestination}] deleted.", this, Enums.GPALObjectType.GPALFile);
808 }
809 else
810 {
811 File.Delete(myDestination);
812 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{myDestination}] deleted.", this, Enums.GPALObjectType.GPALFile);
813 }
814 }
815 catch (Exception ex)
816 {
817 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Unable to delete[{myDestination}].", this, Enums.GPALObjectType.GPALFile, ex);
818 }
819 }
820
824 public void Delete()
825 {
826 foreach (string filename in ((IGPALFileInternal)this).FileSettings.Filenames)
827 {
828 try
829 {
830 File.Delete(filename);
831 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{filename}] deleted.", this, Enums.GPALObjectType.GPALFile);
832 }
833 catch (Exception ex)
834 {
835 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Unable to delete [{filename}].", this, Enums.GPALObjectType.GPALFile, ex);
836 }
837 }
838 }
839
848 internal static bool IsDirectoryOnly(string path)
849 {
850 // the question is about path itself, never about its parent. it named a directory when it says so with
851 // a trailing separator, when it already exists as one, or when it carries no extension to suggest a
852 // filename.
853 // CAVEAT: a directory name containing a dot reads as a file by that last rule. Create such a directory
854 // beforehand and the "already exists" test settles it first.
855 bool isDirectory = true == path.EndsWith(Path.DirectorySeparatorChar.ToString())
856 || true == path.EndsWith(Path.AltDirectorySeparatorChar.ToString())
857 || true == Directory.Exists(path)
858 || false == Path.HasExtension(path);
859
860 // either way the folder has to be there before anything is written: the path itself when it is a
861 // directory, its parent when it is a file
862 string directoryToCreate = true == isDirectory ? path : Path.GetDirectoryName(path);
863
864 if (false == string.IsNullOrEmpty(directoryToCreate) && false == Directory.Exists(directoryToCreate))
865 {
866 Directory.CreateDirectory(directoryToCreate);
867 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{directoryToCreate}] created");
868 }
869
870 return isDirectory;
871 }
872
878 internal GPALFile Clone()
879 {
880 GPALFile clone = new GPALFile();
881 IGPALFileInternal cloneInternal = (IGPALFileInternal)clone;
882 IGPALFileInternal thisInternal = (IGPALFileInternal)this;
883
884 // Manually copy FileSettings properties
885 cloneInternal.FileSettings = new GPALFileSettings
886 {
887 Filenames = new List<string>(thisInternal.FileSettings.Filenames),
888 FileCount = thisInternal.FileSettings.FileCount,
889 NextFileCount = thisInternal.FileSettings.NextFileCount,
890 Delimiter = new List<char?>(thisInternal.FileSettings.Delimiter),
891 FieldsEnclosedInQuotes = new List<bool>(thisInternal.FileSettings.FieldsEnclosedInQuotes),
892 FirstLineIsColumnNames = new List<bool>(thisInternal.FileSettings.FirstLineIsColumnNames),
893 IgnoreFirstLineColumnNames = new List<bool>(thisInternal.FileSettings.IgnoreFirstLineColumnNames),
894 UseOneHeaderForAllFiles = thisInternal.FileSettings.UseOneHeaderForAllFiles,
895 TokenList = thisInternal.FileSettings.TokenList != null ? new List<IGPALGrid<string>>(thisInternal.FileSettings.TokenList) : null,
896 FileData = thisInternal.FileSettings.FileData != null ? new List<string>(thisInternal.FileSettings.FileData) : null,
897 FileDictionary = thisInternal.FileSettings.FileDictionary != null ? new List<Dictionary<object, dynamic>>(thisInternal.FileSettings.FileDictionary) : null,
898 FileGrid = thisInternal.FileSettings.FileGrid != null ? new List<IGPALGrid<string>>(thisInternal.FileSettings.FileGrid) : null,
899 AlreadyTokenized = thisInternal.FileSettings.AlreadyTokenized
900 };
901
902 // Copy FileDictionary directly (it's already a list of dictionaries)
903 cloneInternal.FileDictionary = thisInternal.FileDictionary != null ? new List<Dictionary<object, dynamic>>(thisInternal.FileDictionary) : null;
904
905 return clone;
906 }
907 #endregion Helpers
908 #region Implicit Converter
915 public static implicit operator string(GPALFile url) => url.Filename;
916
917 // Implicit conversion from string to GPALFile, so a filename string
918 // can be passed directly where GPALFile is expected
925 public static implicit operator GPALFile(string fileName)
926 {
927 var file = new GPALFile(); // uses parameterless constructor
928
929 if (string.IsNullOrWhiteSpace(fileName))
930 {
932 GPALEventType.DEBUG,
933 "Implicit conversion received null or whitespace filename - " +
934 "creating placeholder GPALFile with no real filename.",
935 "(null or empty)",
936 GPALObjectType.GPALFile
937 );
938
939 file.WithFileName("Placeholder_Empty_" + DateTime.UtcNow.Ticks);
940 return file;
941 }
942
943 string trimmed = fileName.Trim();
944
945 // Add the filename using the existing fluent method
946 file.WithFileName(trimmed);
947
948 // Give it a recognizable name for debugging/logs
949 string shortName = trimmed.Length > 48
950 ? trimmed.Substring(0, 45) + "..."
951 : trimmed;
952
954 GPALEventType.DEBUG,
955 $"Implicit GPALFile created from [{shortName}]."
956 );
957
958 return file;
959 }
960 #endregion Implicit Converter
961 }
962
967 internal class NaturalFilenameComparer : IComparer<string>
968 {
969 public static readonly NaturalFilenameComparer Instance = new NaturalFilenameComparer();
970
971 public int Compare(string x, string y)
972 {
973 IEnumerable<string> partsX = SplitIntoParts(x);
974 IEnumerable<string> partsY = SplitIntoParts(y);
975
976 using (var enumX = partsX.GetEnumerator())
977 using (var enumY = partsY.GetEnumerator())
978 {
979 while (true)
980 {
981 bool hasX = enumX.MoveNext();
982 bool hasY = enumY.MoveNext();
983
984 if (false == hasX && false == hasY)
985 return 0;
986 if (false == hasX)
987 return -1;
988 if (false == hasY)
989 return 1;
990
991 string partX = enumX.Current;
992 string partY = enumY.Current;
993
994 bool isNumericX = char.IsDigit(partX[0]);
995 bool isNumericY = char.IsDigit(partY[0]);
996
997 int comparison;
998
999 if (true == isNumericX && true == isNumericY)
1000 {
1001 // compare numerically, falling back to length for very large numbers
1002 BigInteger numX = BigInteger.Parse(partX);
1003 BigInteger numY = BigInteger.Parse(partY);
1004 comparison = numX.CompareTo(numY);
1005 }
1006 else
1007 {
1008 comparison = string.Compare(partX, partY, StringComparison.OrdinalIgnoreCase);
1009 }
1010
1011 if (0 != comparison)
1012 return comparison;
1013 }
1014 }
1015 }
1016
1017 private static IEnumerable<string> SplitIntoParts(string value)
1018 {
1019 int index = 0;
1020
1021 while (index < value.Length)
1022 {
1023 bool isDigit = char.IsDigit(value[index]);
1024 int start = index;
1025
1026 while (index < value.Length && char.IsDigit(value[index]) == isDigit)
1027 index++;
1028
1029 yield return value.Substring(start, index - start);
1030 }
1031 }
1032 }
1033}
1034
File-side plumbing behind the fluent chain: writing a unit of work's data out in a delimited format,...
Definition FileHelper.cs:55
GPAL File object instantied with GPAL.File Used to load tokens into a GPALGrid [rows/columns].
Definition GPALFile.cs:36
IAllowFileSettings WithColumnsEnclosedInQuotes(bool fieldsInQuotes)
Specifies whether each field (column value) in the file is enclosed in quotes. Applies to flat files ...
Definition GPALFile.cs:125
void CopyTo(string destination)
Copies each file in Filenames to destination . If destination is a directory (per IsDirectoryOnly(st...
Definition GPALFile.cs:729
void MoveTo(string destination)
Moves each file in Filenames to destination . If destination is a directory (per IsDirectoryOnly(str...
Definition GPALFile.cs:763
string DirectoryPart
The folder of the first filename, since most files are singular. Same shortcut Filename is to Filenam...
Definition GPALFile.cs:506
string First
Returns the first filename in Filenames and resets the Next cursor so that a subsequent call to Next ...
Definition GPALFile.cs:369
IAllowFileSettings WithNextFilePattern(Enums.NextFilePattern nextFilePattern=Enums.NextFilePattern.CounterPadded)
Sets how Next builds a new, unique filename when it needs one (the target file already exists,...
Definition GPALFile.cs:104
IAllowFileSettings WithColumnName(string columnName)
.WithColumnName adds columns to the same 'most recent' column list. NOTE: .WithColumnNameS allows You...
Definition GPALFile.cs:292
List< string > DirectoryParts
The folder of every filename, in the same order as Filenames, so a wildcard that matched across folde...
Definition GPALFile.cs:485
void Delete()
Deletes every file in Filenames. Errors are reported via GPAL.PublishSimpleEvent rather than thrown.
Definition GPALFile.cs:824
string Last
Returns the last filename in Filenames and positions the Next cursor at that filename so that a subse...
Definition GPALFile.cs:386
List< string > ReturnFilenames
Get the list of filenames saved to (returned).
Definition GPALFile.cs:546
IAllowFileSettings WithFileName(string fileName)
The filename to load input tokens from. An http or https address is fetched to a local file in the w...
Definition GPALFile.cs:202
IAllowFileSettings WithColumnNames(string columnNames)
Add column names to the column name list using the .WithDelimiter to separate names....
Definition GPALFile.cs:311
List< string > FileParts
The name and extension of every filename, without its folder, in the same order as Filenames.
Definition GPALFile.cs:495
GPALFile Next
Advances an internal cursor and returns the next filename to use. If there is exactly one file and Wi...
Definition GPALFile.cs:407
int Count
The number of filenames in Filenames.
Definition GPALFile.cs:716
IAllowFileSettings WithOverwriteFile(bool overwriteFile=false)
Sets whether the destination file(s) should be overwritten or a new filename should be generated (use...
Definition GPALFile.cs:80
IAllowFileSettings WithFileSortOrder(Enums.FileSortOrder fileSortOrder)
Sets the order in which files matched by a wildcard pattern (e.g. *.csv) passed to WithFileName(strin...
Definition GPALFile.cs:92
IAllowFileSettings WithDelimiter(char delimiter)
THe character delimiting each token in the row (columns).
Definition GPALFile.cs:114
IAllowFileSettings WithIgnoreFirstLineColumnNames(bool ignoreFirstLine=true)
Specifies that the first line of the file (a column header row) should be ignored/skipped when readin...
Definition GPALFile.cs:145
void Delete(string destination)
Deletes the file at destination , or if it is a directory (per IsDirectoryOnly(string)),...
Definition GPALFile.cs:795
IAllowFileSettings WithBrowser(Browser.IBrowser browser)
The browser whose session can reach a url that our own connection cannot, for a file behind a login o...
Definition GPALFile.cs:180
string FilePart
The name and extension of the first filename, without its folder. Useful for putting the same name in...
Definition GPALFile.cs:526
GPALFile ToGPALObject()
Returns this GPALFile instance, allowing it to be passed where a generic GPAL object is expected.
Definition GPALFile.cs:48
List< string > Filenames
Get the list of filenames.
Definition GPALFile.cs:536
string SourceUrl
The url this file was fetched from, when .WithFileName was given one. Null for a file that was alread...
Definition GPALFile.cs:558
string Filename
We have only one file, accessing it.
Definition GPALFile.cs:474
IAllowFileSettings WithUseSameColumnNamesForAllFiles(bool useHeaderForAllFiles)
Specifies that a single column name list has been defined and should be reused for every file in the ...
Definition GPALFile.cs:156
IAllowFileSettings WithColumnNames(string[] columnNames)
Add column names to the column name list using the .WithDelimiter to separate names....
Definition GPALFile.cs:341
IAllowFileSettings WithFirstLineIsColumnNames(bool firstLineIsColumnNames)
Specifies that the first line of the file contains column header names.
Definition GPALFile.cs:135
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2406