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(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.ColumnList.Count() - 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.ColumnList.Count() - 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.ColumnList.Count() - 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 foreach (string columnName in columnNames)
348 ((IGPALFileInternal)this).FileSettings.ColumnList[((IGPALFileInternal)this).FileSettings.ColumnList.Count() - 1].Add(columnName);
349
350 if (((IGPALFileInternal)this).FileSettings.FileCount > ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames.Count)
351 ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames.Add(true);
352
353 return this;
354 }
355 #endregion <Fluent Interface>
356
357 #region <Getters/Setters>
362 public string First
363 {
364 get
365 {
366 if (0 < ((IGPALFileInternal)this).FileSettings.FileCount)
367 {
368 ((IGPALFileInternal)this).FileSettings.NextFileCount = 0;
369 return ((IGPALFileInternal)this).FileSettings.Filenames[0];
370 }
371 else
372 return null;
373 }
374 }
375
379 public string Last
380 {
381 get
382 {
383 if (0 < ((IGPALFileInternal)this).FileSettings.FileCount)
384 {
385 ((IGPALFileInternal)this).FileSettings.NextFileCount = ((IGPALFileInternal)this).FileSettings.Filenames.Count - 1;
386 return ((IGPALFileInternal)this).FileSettings.Filenames[((IGPALFileInternal)this).FileSettings.Filenames.Count - 1];
387 }
388 else
389 return null;
390 }
391 }
392
400 public GPALFile Next
401 {
402 get
403 {
404 string fullPath = null;
405 string newFilename = null;
406 Enums.NextFilePattern nextPattern = ((IGPALFileInternal)this).FileSettings.NextFilePattern;
407 int patternCounter = ((IGPALFileInternal)this).FileSettings.NextPatternCounter;
408
409 if (0 == ((IGPALFileInternal)this).FileSettings.OverwriteFile.Count) // default to not overwrite since user did not specify but called next
410 {
411 if (((IGPALFileInternal)this).FileSettings.NextFileCount < ((IGPALFileInternal)this).FileSettings.FileCount) // otherwise get the next filename in the list
412 {
413 fullPath = FileHelper.CleanUpFileDestination(((IGPALFileInternal)this).FileSettings.Filenames[((IGPALFileInternal)this).FileSettings.NextFileCount], out newFilename, DeleteFileBeforeDownload: false, nextPattern, ref patternCounter);
414 ((IGPALFileInternal)this).FileSettings.NextPatternCounter = patternCounter;
415 // record it whether or not the name needed uniquifying. ReturnFilenames is the list of files
416 // we wrote, and a name that came back unchanged still gets written
417 ((IGPALFileInternal)this).FileSettings.ReturnFilenames.Add(fullPath);
418 ((IGPALFileInternal)this).FileSettings.NextFileCount++;
419 return fullPath;
420 }
421 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
422 {
423 fullPath = FileHelper.CleanUpFileDestination(((IGPALFileInternal)this).FileSettings.Filenames[((IGPALFileInternal)this).FileSettings.NextFileCount-1], out newFilename, DeleteFileBeforeDownload: false, nextPattern, ref patternCounter);
424 ((IGPALFileInternal)this).FileSettings.NextPatternCounter = patternCounter;
425 ((IGPALFileInternal)this).FileSettings.ReturnFilenames.Add(fullPath);
426 //((IGPALFileInternal)this).FileSettings.NextFileCount++;
427 return fullPath;
428 }
429 }
430 // do the same but respect the overwrite flag settings
431 else if (((IGPALFileInternal)this).FileSettings.NextFileCount < ((IGPALFileInternal)this).FileSettings.OverwriteFile.Count)
432 {
433 if (true == ((IGPALFileInternal)this).FileSettings.OverwriteFile[((IGPALFileInternal)this).FileSettings.NextFileCount]) // file flagged to overwrite - always return the same filename
434 {
435 fullPath = FileHelper.CleanUpFileDestination(((IGPALFileInternal)this).FileSettings.Filenames[((IGPALFileInternal)this).FileSettings.NextFileCount++], out newFilename, DeleteFileBeforeDownload: true);
436 ((IGPALFileInternal)this).FileSettings.ReturnFilenames.Add(fullPath);
437 return fullPath;
438 }
439 // a remaining user-supplied filename takes priority over generating a new one
440 else if (((IGPALFileInternal)this).FileSettings.NextFileCount < ((IGPALFileInternal)this).FileSettings.FileCount)
441 {
442 fullPath = FileHelper.CleanUpFileDestination(((IGPALFileInternal)this).FileSettings.Filenames[((IGPALFileInternal)this).FileSettings.NextFileCount], out newFilename, DeleteFileBeforeDownload: false, nextPattern, ref patternCounter);
443 ((IGPALFileInternal)this).FileSettings.NextPatternCounter = patternCounter;
444 // record it whether or not the name needed uniquifying. ReturnFilenames is the list of files
445 // we wrote, and a name that came back unchanged still gets written
446 ((IGPALFileInternal)this).FileSettings.ReturnFilenames.Add(fullPath);
447 ((IGPALFileInternal)this).FileSettings.NextFileCount++;
448 return fullPath;
449 }
450 }
451
452 // default to generating a new filename if we fall thru. the pattern is the last filename the
453 // workflow supplied once the cursor is past them, since Filenames only ever holds those now and
454 // deriving from a generated name is what produced base_0001_0003 style names
455 int patternIdx = Math.Min(((IGPALFileInternal)this).FileSettings.NextFileCount, ((IGPALFileInternal)this).FileSettings.FileCount - 1);
456
457 fullPath = FileHelper.CleanUpFileDestination(((IGPALFileInternal)this).FileSettings.Filenames[patternIdx], out newFilename, DeleteFileBeforeDownload: false, nextPattern, ref patternCounter);
458 ((IGPALFileInternal)this).FileSettings.NextPatternCounter = patternCounter;
459 ((IGPALFileInternal)this).FileSettings.ReturnFilenames.Add(fullPath);
460 //((IGPALFileInternal)this).FileSettings.NextFileCount++;
461 return fullPath;
462 }
463 }
464
467 public string Filename
468 {
469 get
470 {
471 // a wildcard that matched nothing leaves no filenames at all, and indexing the first one
472 // there threw out of the implicit string conversion where no caller can see it coming.
473 // WithFileName already published the warning naming the pattern
474 List<string> filenames = ((IGPALFileInternal)this).FileSettings.Filenames;
475
476 return 0 == filenames.Count ? string.Empty : filenames[0];
477 }
478 }
479
483 public List<string> DirectoryParts
484 {
485 get
486 {
487 return ((IGPALFileInternal)this).FileSettings.Filenames.Select(filename => Path.GetDirectoryName(filename)).ToList();
488 }
489 }
490
493 public List<string> FileParts
494 {
495 get
496 {
497 return ((IGPALFileInternal)this).FileSettings.Filenames.Select(filename => Path.GetFileName(filename)).ToList();
498 }
499 }
500
504 public string DirectoryPart
505 {
506 get
507 {
508 // Path.GetDirectoryName throws on an empty path, which is what a no-match wildcard leaves
509 string filename = Filename;
510
511 return 0 == filename.Length ? string.Empty : Path.GetDirectoryName(filename);
512 }
513 }
514
527 public string FilePart
528 {
529 get
530 {
531 string filename = Filename;
532
533 return 0 == filename.Length ? string.Empty : Path.GetFileName(filename);
534 }
535 }
536
539 public List<string> Filenames
540 {
541 get
542 {
543 return ((IGPALFileInternal)this).FileSettings.Filenames;
544 }
545 }
546
549 public List<string> ReturnFilenames
550 {
551 get
552 {
553 return ((IGPALFileInternal)this).FileSettings.ReturnFilenames;
554 }
555 }
556
561 public string SourceUrl
562 {
563 get
564 {
565 return ((IGPALFileInternal)this).FileSettings.SourceUrl;
566 }
567 }
568
573 List<IGPALGrid<string>> IGPALFileInternal.TokenList
574 {
575 get
576 {
577 return ((IGPALFileInternal)this).FileSettings.TokenList;
578 }
579 set
580 {
581 ((IGPALFileInternal)this).FileSettings.TokenList = value;
582 }
583 }
587 List<string> IGPALFileInternal.FileData
588 {
589 get
590 {
591 return ((IGPALFileInternal)this).FileSettings.FileData;
592 }
593 set
594 {
595 ((IGPALFileInternal)this).FileSettings.FileData = value;
596 }
597 }
601 List<char?> IGPALFileInternal.Delimiter
602 {
603 get
604 {
605 return ((IGPALFileInternal)this).FileSettings.Delimiter;
606 }
607 set
608 {
609 ((IGPALFileInternal)this).FileSettings.Delimiter = value;
610 }
611 }
616 List<bool> IGPALFileInternal.FieldsEnclosedInQuotes
617 {
618 get
619 {
620 return ((IGPALFileInternal)this).FileSettings.FieldsEnclosedInQuotes;
621 }
622 set
623 {
624 ((IGPALFileInternal)this).FileSettings.FieldsEnclosedInQuotes = value;
625 }
626 }
631 List<bool> IGPALFileInternal.FirstLineIsColumnNames
632 {
633 get
634 {
635 return ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames;
636 }
637 set
638 {
639 ((IGPALFileInternal)this).FileSettings.FirstLineIsColumnNames = value;
640 }
641 }
646 List<bool> IGPALFileInternal.IgnoreFirstLineColumnNames
647 {
648 get
649 {
650 return ((IGPALFileInternal)this).FileSettings.IgnoreFirstLineColumnNames;
651 }
652 set
653 {
654 ((IGPALFileInternal)this).FileSettings.IgnoreFirstLineColumnNames = value;
655 }
656 }
661 bool IGPALFileInternal.UseOneHeaderForAllFiles
662 {
663 get
664 {
665 return ((IGPALFileInternal)this).FileSettings.UseOneHeaderForAllFiles;
666 }
667 set
668 {
669 ((IGPALFileInternal)this).FileSettings.UseOneHeaderForAllFiles = value;
670 }
671 }
675 List<Dictionary<object, dynamic>> IGPALFileInternal.FileDictionary
676 {
677 get
678 {
679 return ((IGPALFileInternal)this).FileSettings.FileDictionary;
680 }
681 set
682 {
683 ((IGPALFileInternal)this).FileSettings.FileDictionary = value;
684 }
685 }
689 List<IGPALGrid<string>> IGPALFileInternal.FileGrid
690 {
691 get
692 {
693 return ((IGPALFileInternal)this).FileSettings.FileGrid;
694 }
695 set
696 {
697 ((IGPALFileInternal)this).FileSettings.FileGrid = value;
698 }
699 }
704 bool IGPALFileInternal.AlreadyTokenized
705 {
706 get
707 {
708 return ((IGPALFileInternal)this).FileSettings.AlreadyTokenized;
709 }
710
711 set
712 {
713 ((IGPALFileInternal)this).FileSettings.AlreadyTokenized = value;
714 }
715 }
719 public int Count
720 {
721 get => Filenames.Count;
722 }
723 #endregion <Getters/Setters>
724 #region Helpers
733 public void CopyTo(string destination)
734 {
735 string myDestination = destination;
736 bool isDir = IsDirectoryOnly(destination);
737
738 foreach (string filename in ((IGPALFileInternal)this).FileSettings.Filenames)
739 {
740 try
741 {
742 string directory = Path.GetDirectoryName(filename);
743 string filenamepart = Path.GetFileName(filename);
744
745 foreach (string filename2 in Directory.GetFiles(false == string.IsNullOrEmpty(directory) ? directory : @".\", filenamepart))
746 {
747 if (true == isDir)
748 myDestination = Path.Combine(destination, Path.GetFileName(filename2));
749 File.Copy(filename2, myDestination);
750 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{filename2}] copied to [{destination}].", this, Enums.GPALObjectType.GPALFile);
751 }
752 }
753 catch (Exception ex)
754 {
755 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Unable to copy [{filename}] to [{destination}].", this, Enums.GPALObjectType.GPALFile, ex);
756 }
757 }
758 }
759
767 public void MoveTo(string destination)
768 {
769 string myDestination = destination;
770 bool isDir = IsDirectoryOnly(destination);
771
772 foreach (string filename in ((IGPALFileInternal)this).FileSettings.Filenames)
773 {
774 try
775 {
776 string directory = Path.GetDirectoryName(filename);
777 string filenamepart = Path.GetFileName(filename);
778
779 foreach (string filename2 in Directory.GetFiles(false == string.IsNullOrEmpty(directory) ? directory : @".\", filenamepart))
780 {
781 if (true == isDir)
782 myDestination = Path.Combine(destination, Path.GetFileName(filename2));
783 File.Move(filename2, myDestination);
784 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{filename2}] moved to [{destination}].", this, Enums.GPALObjectType.GPALFile);
785 }
786 }
787 catch (Exception ex)
788 {
789 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Unable to move [{filename}] to [{destination}].", this, Enums.GPALObjectType.GPALFile, ex);
790 }
791 }
792 }
793
799 public void Delete(string destination)
800 {
801 string myDestination = destination;
802 bool isDir = IsDirectoryOnly(destination);
803
804 try
805 {
806 if (true == isDir)
807 foreach (string filename in Directory.GetFiles(destination))
808 {
809 myDestination = Path.Combine(destination, filename);
810 File.Delete(Path.Combine(myDestination, filename));
811 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{myDestination}] deleted.", this, Enums.GPALObjectType.GPALFile);
812 }
813 else
814 {
815 File.Delete(myDestination);
816 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{myDestination}] deleted.", this, Enums.GPALObjectType.GPALFile);
817 }
818 }
819 catch (Exception ex)
820 {
821 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Unable to delete[{myDestination}].", this, Enums.GPALObjectType.GPALFile, ex);
822 }
823 }
824
828 public void Delete()
829 {
830 foreach (string filename in ((IGPALFileInternal)this).FileSettings.Filenames)
831 {
832 try
833 {
834 File.Delete(filename);
835 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{filename}] deleted.", this, Enums.GPALObjectType.GPALFile);
836 }
837 catch (Exception ex)
838 {
839 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Unable to delete [{filename}].", this, Enums.GPALObjectType.GPALFile, ex);
840 }
841 }
842 }
843
852 internal static bool IsDirectoryOnly(string path)
853 {
854 // the question is about path itself, never about its parent. it named a directory when it says so with
855 // a trailing separator, when it already exists as one, or when it carries no extension to suggest a
856 // filename.
857 // CAVEAT: a directory name containing a dot reads as a file by that last rule. Create such a directory
858 // beforehand and the "already exists" test settles it first.
859 bool isDirectory = true == path.EndsWith(Path.DirectorySeparatorChar.ToString())
860 || true == path.EndsWith(Path.AltDirectorySeparatorChar.ToString())
861 || true == Directory.Exists(path)
862 || false == Path.HasExtension(path);
863
864 // either way the folder has to be there before anything is written: the path itself when it is a
865 // directory, its parent when it is a file
866 string directoryToCreate = true == isDirectory ? path : Path.GetDirectoryName(path);
867
868 if (false == string.IsNullOrEmpty(directoryToCreate) && false == Directory.Exists(directoryToCreate))
869 {
870 Directory.CreateDirectory(directoryToCreate);
871 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{directoryToCreate}] created");
872 }
873
874 return isDirectory;
875 }
876
882 internal GPALFile Clone()
883 {
884 GPALFile clone = new GPALFile();
885 IGPALFileInternal cloneInternal = (IGPALFileInternal)clone;
886 IGPALFileInternal thisInternal = (IGPALFileInternal)this;
887
888 // Manually copy FileSettings properties
889 cloneInternal.FileSettings = new GPALFileSettings
890 {
891 Filenames = new List<string>(thisInternal.FileSettings.Filenames),
892 FileCount = thisInternal.FileSettings.FileCount,
893 NextFileCount = thisInternal.FileSettings.NextFileCount,
894 Delimiter = new List<char?>(thisInternal.FileSettings.Delimiter),
895 FieldsEnclosedInQuotes = new List<bool>(thisInternal.FileSettings.FieldsEnclosedInQuotes),
896 FirstLineIsColumnNames = new List<bool>(thisInternal.FileSettings.FirstLineIsColumnNames),
897 IgnoreFirstLineColumnNames = new List<bool>(thisInternal.FileSettings.IgnoreFirstLineColumnNames),
898 UseOneHeaderForAllFiles = thisInternal.FileSettings.UseOneHeaderForAllFiles,
899 TokenList = thisInternal.FileSettings.TokenList != null ? new List<IGPALGrid<string>>(thisInternal.FileSettings.TokenList) : null,
900 FileData = thisInternal.FileSettings.FileData != null ? new List<string>(thisInternal.FileSettings.FileData) : null,
901 FileDictionary = thisInternal.FileSettings.FileDictionary != null ? new List<Dictionary<object, dynamic>>(thisInternal.FileSettings.FileDictionary) : null,
902 FileGrid = thisInternal.FileSettings.FileGrid != null ? new List<IGPALGrid<string>>(thisInternal.FileSettings.FileGrid) : null,
903 AlreadyTokenized = thisInternal.FileSettings.AlreadyTokenized
904 };
905
906 // Copy FileDictionary directly (it's already a list of dictionaries)
907 cloneInternal.FileDictionary = thisInternal.FileDictionary != null ? new List<Dictionary<object, dynamic>>(thisInternal.FileDictionary) : null;
908
909 return clone;
910 }
911 #endregion Helpers
912 #region Implicit Converter
919 public static implicit operator string(GPALFile url) => url.Filename;
920
921 // Implicit conversion from string to GPALFile, so a filename string
922 // can be passed directly where GPALFile is expected
929 public static implicit operator GPALFile(string fileName)
930 {
931 var file = new GPALFile(); // uses parameterless constructor
932
933 if (string.IsNullOrWhiteSpace(fileName))
934 {
936 GPALEventType.DEBUG,
937 "Implicit conversion received null or whitespace filename - " +
938 "creating placeholder GPALFile with no real filename.",
939 "(null or empty)",
940 GPALObjectType.GPALFile
941 );
942
943 file.WithFileName("Placeholder_Empty_" + DateTime.UtcNow.Ticks);
944 return file;
945 }
946
947 string trimmed = fileName.Trim();
948
949 // Add the filename using the existing fluent method
950 file.WithFileName(trimmed);
951
952 // Give it a recognizable name for debugging/logs
953 string shortName = trimmed.Length > 48
954 ? trimmed.Substring(0, 45) + "..."
955 : trimmed;
956
958 GPALEventType.DEBUG,
959 $"Implicit GPALFile created from [{shortName}]."
960 );
961
962 return file;
963 }
964 #endregion Implicit Converter
965 }
966
971 internal class NaturalFilenameComparer : IComparer<string>
972 {
973 public static readonly NaturalFilenameComparer Instance = new NaturalFilenameComparer();
974
975 public int Compare(string x, string y)
976 {
977 IEnumerable<string> partsX = SplitIntoParts(x);
978 IEnumerable<string> partsY = SplitIntoParts(y);
979
980 using (var enumX = partsX.GetEnumerator())
981 using (var enumY = partsY.GetEnumerator())
982 {
983 while (true)
984 {
985 bool hasX = enumX.MoveNext();
986 bool hasY = enumY.MoveNext();
987
988 if (false == hasX && false == hasY)
989 return 0;
990 if (false == hasX)
991 return -1;
992 if (false == hasY)
993 return 1;
994
995 string partX = enumX.Current;
996 string partY = enumY.Current;
997
998 bool isNumericX = char.IsDigit(partX[0]);
999 bool isNumericY = char.IsDigit(partY[0]);
1000
1001 int comparison;
1002
1003 if (true == isNumericX && true == isNumericY)
1004 {
1005 // compare numerically, falling back to length for very large numbers
1006 BigInteger numX = BigInteger.Parse(partX);
1007 BigInteger numY = BigInteger.Parse(partY);
1008 comparison = numX.CompareTo(numY);
1009 }
1010 else
1011 {
1012 comparison = string.Compare(partX, partY, StringComparison.OrdinalIgnoreCase);
1013 }
1014
1015 if (0 != comparison)
1016 return comparison;
1017 }
1018 }
1019 }
1020
1021 private static IEnumerable<string> SplitIntoParts(string value)
1022 {
1023 int index = 0;
1024
1025 while (index < value.Length)
1026 {
1027 bool isDigit = char.IsDigit(value[index]);
1028 int start = index;
1029
1030 while (index < value.Length && char.IsDigit(value[index]) == isDigit)
1031 index++;
1032
1033 yield return value.Substring(start, index - start);
1034 }
1035 }
1036 }
1037}
1038
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:733
void MoveTo(string destination)
Moves each file in Filenames to destination . If destination is a directory (per IsDirectoryOnly(str...
Definition GPALFile.cs:767
string DirectoryPart
The folder of the first filename, since most files are singular. Same shortcut Filename is to Filenam...
Definition GPALFile.cs:505
string First
Returns the first filename in Filenames and resets the Next cursor so that a subsequent call to Next ...
Definition GPALFile.cs:363
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:484
void Delete()
Deletes every file in Filenames. Errors are reported via GPAL.PublishSimpleEvent rather than thrown.
Definition GPALFile.cs:828
string Last
Returns the last filename in Filenames and positions the Next cursor at that filename so that a subse...
Definition GPALFile.cs:380
List< string > ReturnFilenames
Get the list of filenames saved to (returned).
Definition GPALFile.cs:550
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:494
GPALFile Next
Advances an internal cursor and returns the next filename to use. If there is exactly one file and Wi...
Definition GPALFile.cs:401
int Count
The number of filenames in Filenames.
Definition GPALFile.cs:720
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:799
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:528
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:540
string SourceUrl
The url this file was fetched from, when .WithFileName was given one. Null for a file that was alread...
Definition GPALFile.cs:562
string Filename
We have only one file, accessing it.
Definition GPALFile.cs:468
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:2568