58 private static readonly HashSet<string> _loggedMessages =
new HashSet<string>();
59 private static bool _suppressNoticeShown =
false;
65 private static void PublishSuppressingRepeats(GPALEventType type,
string message,
object source, GPALObjectType objectType)
67 if (
true == _loggedMessages.Add(message))
69 else if (
false == _suppressNoticeShown)
72 _suppressNoticeShown =
true;
81 internal static int GetGrid(GPALFileSettings fileSettings, out List<IGPALGrid<string>> tokens)
83 FileStream fileStream =
null;
84 StreamReader streamReader =
null;
87 List<IGPALGrid<string>> myTokens =
new List<IGPALGrid<string>>();
88 IGPALGrid<string> myGrid =
GPAL.
Grid.ToGPALObject();
90 foreach (
string filename
in fileSettings.Filenames)
93 string directory = Path.GetDirectoryName(filename);
94 string filenamepart = Path.GetFileName(filename);
95 if (
true == File.Exists(filename))
96 foreach (
string filename2
in Directory.GetFiles(
false ==
string.IsNullOrEmpty(directory) ? directory :
@".\", filenamepart))
98 bool firstLineSkipped =
false;
100 using (fileStream = System.IO.File.OpenRead(filename2))
102 using (streamReader =
new StreamReader(fileStream))
104 while (
null != (currentLine = streamReader?.ReadLine()))
106 if (
false == firstLineSkipped && 0 < fileSettings.IgnoreFirstLineColumnNames.Count && fileSettings.IgnoreFirstLineColumnNames[0])
108 firstLineSkipped =
true;
111 List<string> currentRow =
new List<string>();
114 if (0 < fileSettings.Delimiter.Count &&
true == fileSettings.Delimiter[fileIdx].HasValue)
115 delim = fileSettings.Delimiter[fileIdx].Value;
118 PublishSuppressingRepeats(GPALEventType.INFO, $
"[{filename}] has no delimiter set, using comma.", fileSettings, GPALObjectType.FileSettings);
122 foreach (
string token
in ConverterHelper.SplitRespectingQuotes(currentLine, delim))
123 currentRow.Add(token);
125 myGrid.AddRow(currentRow);
127 myTokens.Add(myGrid);
128 fileSettings.FileGrid.Add(myGrid);
136 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to tokenize File [{filename}]. Tokens created [{myGrid.Rows}", fileSettings, GPALObjectType.FileSettings, ex);
140 streamReader?.Close();
144 return tokens.Count();
153 internal static string CleanUpFileDestination(
string filename, out
string newFilename,
bool DeleteFileBeforeDownload)
156 return CleanUpFileDestination(filename, out newFilename, DeleteFileBeforeDownload, Enums.NextFilePattern.Timestamp, ref counter);
164 internal static string CleanUpFileDestination(
string filename, out
string newFilename,
bool DeleteFileBeforeDownload,
165 Enums.NextFilePattern nextFilePattern, ref
int patternCounter)
168 string currentDirectory = Path.GetDirectoryName(filename);
170 newFilename = filename;
171 if (
true ==
string.IsNullOrEmpty(currentDirectory))
173 currentDirectory =
".\\";
174 fullFilePath = currentDirectory + filename;
177 fullFilePath = filename;
179 if (
true == DeleteFileBeforeDownload)
183 if (
true == File.Exists(fullFilePath))
185 System.IO.File.Delete(fullFilePath);
186 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Deleted file [{fullFilePath}].",
null, GPALObjectType.None);
191 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to delete file [{fullFilePath}], Exception caught.",
null, GPALObjectType.None, ex);
194 else if (
true == System.IO.File.Exists(fullFilePath))
196 string baseName = Path.GetFileNameWithoutExtension(filename);
197 string extension = Path.GetExtension(filename);
199 if (Enums.NextFilePattern.Counter == nextFilePattern || Enums.NextFilePattern.CounterPadded == nextFilePattern)
205 if (0 == patternCounter)
206 patternCounter = HighestCounterIndex(currentDirectory, baseName, extension);
211 newFilename =
string.Concat(baseName, NextFileSuffix(nextFilePattern, ref patternCounter), extension);
212 fullFilePath = currentDirectory +
"\\" + newFilename;
214 while (
true == System.IO.File.Exists(fullFilePath));
219 newFilename =
string.Concat(baseName, NextFileSuffix(nextFilePattern, ref patternCounter), extension);
220 fullFilePath = currentDirectory +
"\\" + newFilename;
231 private static int HighestCounterIndex(
string directory,
string baseName,
string extension)
237 string searchDir =
string.IsNullOrEmpty(directory) ?
"." : directory;
239 string searchPattern = baseName +
"_*" + extension;
241 foreach (
string path
in Directory.EnumerateFiles(searchDir, searchPattern))
243 string name = Path.GetFileNameWithoutExtension(path);
244 int underscore = name.LastIndexOf(
'_');
245 if (0 > underscore || underscore + 1 >= name.Length)
248 string digits = name.Substring(underscore + 1);
249 if (
true == digits.All(
char.IsDigit) &&
true ==
int.TryParse(digits, out
int n) && n > max)
265 private static string NextFileSuffix(Enums.NextFilePattern pattern, ref
int counter)
269 case Enums.NextFilePattern.DateTimeStamp:
270 return "_" + DateTime.Now.ToString(
"yyyyMMdd_HHmmss");
271 case Enums.NextFilePattern.DateStamp:
272 return "_" + DateTime.Now.ToString(
"yyyyMMdd");
273 case Enums.NextFilePattern.Counter:
274 return "_" + (++counter).ToString();
275 case Enums.NextFilePattern.CounterPadded:
277 return "_" + (++counter).ToString(
"D4");
278 case Enums.NextFilePattern.Timestamp:
280 return DateTime.Now.ToString(
"yyyyMMddHHmmssfff");
297 public static string SaveToDelimited(
UnitOfWork currentUOW,
string filename,
char delim,
bool appendToFile,
bool DeleteFileBeforeDownload)
301 string fullFilePath = CleanUpFileDestination(filename, out
string newFilename, DeleteFileBeforeDownload :
false);
305 foreach (
Selector selector
in selectors)
308 if (
null != selector.
Name)
312 else if (selectors.Count > currentUOW.
HeaderList.Count)
314 for (
int idx = currentUOW.
HeaderList.Count; idx < selectors.Count; idx++)
315 currentUOW.
HeaderList.Add(selectors[idx].Name);
317 else if (selectors.Count < currentUOW.
HeaderList.Count)
319 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Too many headers, not all will be output. This may not be correct if you are saving your own grid.", currentUOW, GPALObjectType.UnitOfWork);
322 if (
false == appendToFile &&
true == System.IO.File.Exists(fullFilePath))
324 System.IO.File.Delete(fullFilePath);
325 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Asking to save new to existing file [{fullFilePath}], deleting file.", currentUOW, GPALObjectType.UnitOfWork);
327 else if (
true == appendToFile &&
false == System.IO.File.Exists(fullFilePath))
329 appendToFile =
false;
330 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Asking to append to non-existent file [{fullFilePath}], creating new file with headers.", currentUOW, GPALObjectType.UnitOfWork);
334 using (csv = System.IO.File.AppendText(fullFilePath))
336 StringBuilder tmpRow =
new StringBuilder();
337 if (
false == appendToFile)
340 foreach (
string header
in currentUOW.
HeaderList)
343 tmpRow.Append(header);
344 if (colCnt < currentUOW.ColCount)
345 tmpRow.Append(delim);
347 csv.WriteLine(tmpRow);
350 foreach (List<string> row
in currentUOW.RetGrid)
354 foreach (
string col
in row)
358 if (colCnt < currentUOW.ColCount)
359 tmpRow.Append(delim);
361 csv.WriteLine(tmpRow);
381 internal static bool DownloadToFile(
GPALElement webElement,
string filename,
string downloadUrl =
null)
383 bool downloaded =
false;
384 string url = downloadUrl ?? GetDownloadUrl(webElement);
388 using (System.Net.Http.HttpClient client =
new System.Net.Http.HttpClient())
390 System.Net.Http.HttpRequestMessage request =
new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, url);
396 request.Headers.TryAddWithoutValidation(
"User-Agent",
397 true ==
string.IsNullOrWhiteSpace(
GPAL.GPALSettings.UserAgent)
399 :
GPAL.GPALSettings.UserAgent);
401 if (
true == File.Exists(filename))
402 request.Headers.IfModifiedSince = File.GetLastWriteTimeUtc(filename);
404 System.Net.Http.HttpResponseMessage response = client.SendAsync(request).GetAwaiter().GetResult();
406 if (System.Net.HttpStatusCode.NotModified == response.StatusCode)
408 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{url}] has not changed, using [{filename}].",
null, GPALObjectType.None);
411 else if (
false == response.IsSuccessStatusCode)
415 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{url}] returned HTTP [{(int)response.StatusCode}].",
null, GPALObjectType.None);
419 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Downloading [{url}] to [{filename}].",
null, GPALObjectType.None);
420 File.WriteAllBytes(filename, response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult());
424 if (
null != response.Content.Headers.LastModified)
425 File.SetLastWriteTimeUtc(filename, response.Content.Headers.LastModified.Value.UtcDateTime);
436 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{url}] failed to save to [{filename}].",
null, GPALObjectType.None, ex);
446 internal static string GetDownloadUrl(GPALElement element)
449 string downloadUrl =
null;
452 if (element.
TagName.ToLower() ==
"a")
457 else if (element.
TagName.ToLower() ==
"img")
462 else if (element.
TagName.ToLower() ==
"script")
467 else if (element.
TagName.ToLower() ==
"link")
472 else if (element.
TagName.ToLower() ==
"object")
477 else if (element.
TagName.ToLower() ==
"embed")
482 else if (element.
TagName.ToLower() ==
"frame" || element.
TagName.ToLower() ==
"iframe")
487 else if (element.
TagName.ToLower() ==
"audio")
492 else if (element.
TagName.ToLower() ==
"video")
500 internal static FileSystemWatcher watcher;
501 internal static DateTime watchArmed;
502 internal static readonly HashSet<string> watchedScratchFiles =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
504 internal static string SetupDownloadWatcher(
string fullFilePathToSave, Browser.BrowserSettings browserSettings)
510 bool downloadsWhereItLikes =
true == browserSettings.UseOttoMagic
511 || BrowserType.FireFox == browserSettings.BrowserType;
512 string downloadDirectory =
true == downloadsWhereItLikes
513 ? FileHelper.GetDefaultDownloadDirectory(browserSettings.Browser)
514 : Path.GetDirectoryName(fullFilePathToSave) ?? FileHelper.GetDefaultDownloadDirectory(browserSettings.Browser);
516 string fileName = Path.GetFileName(fullFilePathToSave);
517 string actualDownloadedFilename =
null;
519 watchArmed = DateTime.Now;
520 watchedScratchFiles.Clear();
522 watcher =
new FileSystemWatcher(downloadDirectory);
523 watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.Attributes;
524 watcher.Filter =
"*.*";
525 watcher.EnableRaisingEvents =
true;
528 watcher.Created += (sender, e) => handleFile(sender, e, downloadDirectory, fullFilePathToSave);
529 watcher.Changed += (sender, e) => handleFile(sender, e, downloadDirectory, fullFilePathToSave);
530 watcher.Renamed += (sender, e) => handleFile(sender, e, downloadDirectory, fullFilePathToSave);
532 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Watching [{downloadDirectory}] for [{fileName}] w/timeout [{browserSettings.DownloadTimeoutInSec}] secs",
null, GPALObjectType.None);
533 System.Threading.Thread.Sleep(1_000);
535 return downloadDirectory;
537 void handleFile(
object sender, FileSystemEventArgs e,
string downloaddirectory,
string fullFilePathToSave)
544 if (e is RenamedEventArgs renamed
545 && (
true == renamed.OldName.EndsWith(
".crdownload") ||
true == renamed.OldName.EndsWith(
".part")))
547 actualDownloadedFilename = WaitForDownloadToFinish(Path.Combine(downloaddirectory, renamed.Name), browserSettings.DownloadTimeoutInSec);
548 browserSettings.FileDownloaded =
false ==
string.IsNullOrEmpty(actualDownloadedFilename);
550 if (
true == browserSettings.FileDownloaded)
552 File.Move(actualDownloadedFilename, fullFilePathToSave);
553 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{actualDownloadedFilename}] moved to [{fullFilePathToSave}]",
null, GPALObjectType.None);
556 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{renamed.OldName}] was renamed to [{renamed.Name}] but it never settled",
null, GPALObjectType.None);
560 if (
true == browserSettings.FileDownloaded)
561 ((FileSystemWatcher)sender).Dispose();
566 if (e.Name.EndsWith(
".crdownload") || e.Name.EndsWith(
".part"))
568 string filename = e.Name.Replace(
".crdownload",
"").Replace(
".part",
"");
569 string fullSavedFilePath = Path.Combine(downloaddirectory, filename);
571 string pattern =
@"^downloads(?:\.htm|\s*\(\d+\)\.htm|\.htm\s*\(\d+\))$";
572 if (Regex.IsMatch(filename, pattern, RegexOptions.IgnoreCase))
577 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Download started for [{e.Name}] waiting on expected [{fullSavedFilePath}]",
null, GPALObjectType.None);
578 watchedScratchFiles.Add(Path.Combine(downloaddirectory, e.Name));
584 if (
true == File.Exists(Path.Combine(downloaddirectory, e.Name)))
585 actualDownloadedFilename = WaitForDownloadToFinish(Path.Combine(downloaddirectory, e.Name), browserSettings.DownloadTimeoutInSec);
591 if (
true == IsTemporaryDownload(actualDownloadedFilename))
592 actualDownloadedFilename =
null;
596 if (
true == File.Exists(fullFilePathToSave))
597 actualDownloadedFilename = WaitForDownloadToFinish(fullFilePathToSave, browserSettings.DownloadTimeoutInSec);
601 if (
true ==
string.IsNullOrEmpty(actualDownloadedFilename))
602 actualDownloadedFilename = WaitForDownloadToFinish(fullSavedFilePath, browserSettings.DownloadTimeoutInSec);
604 browserSettings.FileDownloaded = !
string.IsNullOrEmpty(actualDownloadedFilename);
606 if (
true == browserSettings.FileDownloaded)
610 if (
true == fullFilePathToSave.Equals(actualDownloadedFilename, StringComparison.OrdinalIgnoreCase))
611 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{fullFilePathToSave}] downloaded straight to where it was asked to go",
null, GPALObjectType.None);
614 File.Move(actualDownloadedFilename, fullFilePathToSave);
615 if (File.Exists(actualDownloadedFilename))
616 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Unable to move [{actualDownloadedFilename}] to [{fullFilePathToSave}]",
null, GPALObjectType.None);
618 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{actualDownloadedFilename}] moved to [{fullFilePathToSave}]",
null, GPALObjectType.None);
623 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"WaitForDownloadToFinish returned null for [{fullSavedFilePath}] - check timeout or stability",
null, GPALObjectType.None);
628 if (
true == browserSettings.FileDownloaded)
629 ((FileSystemWatcher)sender).Dispose();
638 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Error for [{fullFilePathToSave}]",
null, GPALObjectType.None, ex);
649 internal const int DownloadSizeSampleMs = 500;
656 internal static bool IsTemporaryDownload(
string path)
658 bool retVal =
true == path?.EndsWith(
".crdownload") ||
true == path?.EndsWith(
".part");
675 internal static string NewestDownloadSince(
string directory, DateTime since)
677 string retVal =
null;
678 DateTime newest = DateTime.MinValue;
680 foreach (
string candidate
in Directory.EnumerateFiles(directory))
681 if (
false == IsTemporaryDownload(candidate))
687 DateTime written = File.GetLastWriteTime(candidate);
688 DateTime created = File.GetCreationTime(candidate);
689 DateTime arrived = created > written ? created : written;
691 if (arrived >= since && arrived > newest)
701 internal static string WaitForDownloadToFinish(
string filePath,
int downloadTimeoutInSec)
703 long size = 0, newSize = 0;
704 string downloadedFilename;
705 bool fileStarted =
false;
707 newSize = WaitForFileToDownload(filePath, downloadTimeoutInSec, size, out downloadedFilename);
713 System.Threading.Thread.Sleep(1_000);
714 else if (-1 == newSize)
716 if (
false == filePath.EndsWith(
".crdownload") &&
false == filePath.EndsWith(
".part"))
727 if (0 <= (newSize = WaitForFileToDownload(filePath, downloadTimeoutInSec, newSize, out downloadedFilename)))
736 System.Threading.Thread.Sleep(1_000);
741 System.Threading.Thread.Sleep(DownloadSizeSampleMs);
743 if (size == (newSize = WaitForFileToDownload(filePath, downloadTimeoutInSec, newSize, out downloadedFilename)))
748 else if (-1 == newSize)
752 if (
false == filePath.EndsWith(
".crdownload") &&
false == filePath.EndsWith(
".part"))
755 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"File [{downloadedFilename ?? filePath}] downloaded as zero bytes",
null, GPALObjectType.None);
756 else if (-1 == newSize &&
false == fileStarted)
757 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"File [{downloadedFilename ?? filePath}] NOT downloaded",
null, GPALObjectType.None);
761 return downloadedFilename;
763 internal static long WaitForFileToDownload(
string filePath,
int downloadTimeoutInSec,
long size, out
string downloadedFilename)
765 TimeSpan timeout = TimeSpan.FromSeconds(downloadTimeoutInSec);
766 DateTime startTime = DateTime.Now;
767 bool fileExists = Directory.GetFiles(Path.GetDirectoryName(filePath), Path.GetFileName(filePath)).Length > 0;
768 int lastPrinted = -1;
775 if (
false == fileExists &&
true == IsTemporaryDownload(filePath))
777 string arrived = NewestDownloadSince(Path.GetDirectoryName(filePath), watchArmed);
779 if (
true ==
string.IsNullOrEmpty(arrived))
781 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{filePath}] has gone and nothing arrived with it, so the download did not finish.",
null, GPALObjectType.None);
782 downloadedFilename = filePath;
787 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{Path.GetFileName(filePath)}] became [{Path.GetFileName(arrived)}], which is what the browser called it.",
null, GPALObjectType.None);
792 while (
false == fileExists && (DateTime.Now - startTime) < timeout)
794 TimeSpan remaining = timeout - (DateTime.Now - startTime);
795 int remainingSec = (int)Math.Ceiling(remaining.TotalSeconds);
798 if (remainingSec % 10 == 0 && remainingSec != lastPrinted)
799 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Waiting up tp [{remainingSec}] sec for [{filePath}].",
null, GPALObjectType.None);
801 System.Threading.Thread.Sleep(1000);
802 fileExists = Directory.GetFiles(Path.GetDirectoryName(filePath), Path.GetFileName(filePath)).Length > 0;
808 if (
false == fileExists)
810 string arrived = NewestDownloadSince(Path.GetDirectoryName(filePath), watchArmed);
812 if (
false ==
string.IsNullOrEmpty(arrived) &&
false == arrived.Equals(filePath, StringComparison.OrdinalIgnoreCase))
814 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{Path.GetFileName(filePath)}] never showed up, [{Path.GetFileName(arrived)}] did, which is the name the browser gave it.",
null, GPALObjectType.None);
821 if (
false == fileExists)
823 if (
false == filePath.EndsWith(
".crdownload") &&
false == filePath.EndsWith(
".part"))
825 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"File [{filePath}] never showed up.",
null, GPALObjectType.None);
826 downloadedFilename =
null;
829 downloadedFilename = filePath;
839 var cutoff = DateTime.Now.AddMinutes(-2);
841 string newest =
null;
842 DateTime newestTime = DateTime.MinValue;
843 IEnumerable<string> filesFound = Directory.EnumerateFiles(
844 Path.GetDirectoryName(filePath),
845 Path.GetFileName(filePath));
848 if (1 < filesFound.Count())
849 foreach (var f
in filesFound)
852 if (
true == f.Equals(GPAL.GPALSettings.ChromeDriverZipFilename) ||
853 true == f.Equals(GPAL.GPALSettings.EdgeDriverFilename) ||
854 true == f.Equals(GPAL.GPALSettings.FirefoxDriverFilename))
857 var t = File.GetLastWriteTime(f);
859 if (t >= cutoff && t > newestTime)
866 newest = filesFound.FirstOrDefault();
874 downloadedFilename =
true == IsTemporaryDownload(filePath) ? filePath :
null;
879 if (
true == newest.Equals(GPAL.GPALSettings.ChromeDriverZipFilename) ||
880 true == newest.Equals(GPAL.GPALSettings.EdgeDriverFilename) ||
881 true == newest.Equals(GPAL.GPALSettings.FirefoxDriverFilename))
887 downloadedFilename = newest;
892 using (FileStream fs = File.Open(downloadedFilename, FileMode.Open))
895 if (size != fs.Length)
896 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"File [{downloadedFilename}] downloaded with file size [{fs.Length}].",
null, GPALObjectType.None);
907 FileInfo fileInfo =
new FileInfo(downloadedFilename);
912 if (size != fileInfo.Length)
913 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"File [{downloadedFilename}] locked, using FileInfo size [{fileInfo.Length}].",
null, GPALObjectType.None);
915 return fileInfo.Length;
920 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"File [{downloadedFilename}] unable to determine file size.",
null, GPALObjectType.None, ex);
926 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"File [{filePath}] not found.",
null, GPALObjectType.None);
928 downloadedFilename =
null;
942 if (BrowserType.Chrome == browser.BrowserType)
944 else if (BrowserType.Edge == browser.BrowserType)
946 else if (BrowserType.FireFox == browser.BrowserType)
950 string defaultDownloadDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) +
@"\Downloads\";
951 return defaultDownloadDirectory;
962 if (
string.IsNullOrEmpty(profilePath))
964 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
965 profilePath = Path.Combine(localAppData,
@"Google\Chrome\User Data\Default");
968 string prefsPath =
null;
970 if (
false ==
new DirectoryInfo(profilePath).Name.ToLower().Equals(
"default"))
971 prefsPath = Path.Combine(profilePath,
@"default\Preferences");
973 prefsPath = Path.Combine(profilePath,
@"Preferences");
975 if (File.Exists(prefsPath))
977 string json = File.ReadAllText(prefsPath);
978 JObject prefs = JObject.Parse(json);
981 var downloadDir = prefs[
"download"]?[
"default_directory"]?.ToString();
983 if (!
string.IsNullOrEmpty(downloadDir))
987 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
990 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads"); ;
1002 if (
string.IsNullOrEmpty(profilePath))
1004 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
1005 profilePath = Path.Combine(localAppData,
@"Microsoft\Edge\User Data\Default");
1008 string prefsPath = Path.Combine(profilePath,
@"default\Preferences");
1010 if (File.Exists(prefsPath))
1013 string json = File.ReadAllText(prefsPath);
1014 JObject prefs = JObject.Parse(json);
1022 var downloadDir = prefs[
"download"]?[
"default_directory"]?.ToString();
1024 if (!
string.IsNullOrEmpty(downloadDir))
1028 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
1031 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
1042 string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
1043 string firefoxDir = Path.Combine(appData,
@"Mozilla\Firefox");
1044 string profilesIniPath = Path.Combine(firefoxDir,
"profiles.ini");
1046 if (!File.Exists(profilesIniPath))
1050 string[] lines = File.ReadAllLines(profilesIniPath);
1051 string profileDir =
null;
1052 bool isRelative =
true;
1054 for (
int i = 0; i < lines.Length; i++)
1056 string line = lines[i].Trim();
1058 if (line.StartsWith(
"[Profile", StringComparison.OrdinalIgnoreCase))
1061 string currentPath =
null;
1062 bool currentIsRelative =
true;
1063 bool isDefault =
false;
1066 for (
int j = i + 1; j < lines.Length; j++)
1068 string subLine = lines[j].Trim();
1069 if (subLine.StartsWith(
"["))
1072 if (subLine.StartsWith(
"Path=", StringComparison.OrdinalIgnoreCase))
1073 currentPath = subLine.Substring(5);
1074 else if (subLine.StartsWith(
"IsRelative=", StringComparison.OrdinalIgnoreCase))
1075 currentIsRelative = subLine.Substring(11) ==
"1";
1076 else if (subLine.StartsWith(
"Default=", StringComparison.OrdinalIgnoreCase))
1077 isDefault = subLine.Substring(8) ==
"1";
1081 if (isDefault && currentPath !=
null)
1083 profileDir = currentPath;
1084 isRelative = currentIsRelative;
1089 if (profileDir ==
null && currentPath !=
null)
1091 profileDir = currentPath;
1092 isRelative = currentIsRelative;
1097 if (!
string.IsNullOrEmpty(profileDir))
1101 string fullProfilePath = isRelative
1102 ? Path.Combine(firefoxDir, profileDir)
1106 if (!
string.IsNullOrEmpty(profilePath))
1107 fullProfilePath = profilePath;
1109 string prefsJsPath = Path.Combine(fullProfilePath,
"prefs.js");
1111 if (!File.Exists(prefsJsPath))
1115 string content = File.ReadAllText(prefsJsPath);
1118 var match = Regex.Match(content,
@"user_pref\(""browser\.download\.dir"",\s*""([^""]*)""\);");
1122 string dir = match.Groups[1].Value;
1124 return dir.Replace(
"\\\\",
"\\");
1128 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
1132 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
1139 internal static List<IGPALGrid<string>> TokenizeFile(
UnitOfWork currentUOW,
GPALFile inputFile)
1141 currentUOW.InputFile = inputFile;
1142 int rowCount =
FileHelper.GetGrid(((IGPALFileInternal)inputFile).FileSettings, out List<IGPALGrid<string>> tokens);
1150 private static HashSet<string> CollectDictionaryKeys(
object value)
1152 var keys =
new HashSet<string>();
1153 if (value is IList<object> list)
1155 foreach (var item
in list)
1157 if (item is IDictionary dynamicDict)
1159 foreach (dynamic entry
in dynamicDict)
1161 string key = entry.Key?.ToString();
1164 if (entry.Value is IDictionary nestedDict)
1166 foreach (
string nestedKey
in CollectDictionaryKeys(nestedDict))
1168 keys.Add($
"{key}.{nestedKey}");
1180 else if (value is IDictionary dynamicDict)
1182 foreach (dynamic entry
in dynamicDict)
1184 string key = entry.Key?.ToString();
1187 if (entry.Value is IDictionary nestedDict)
1189 foreach (
string nestedKey
in CollectDictionaryKeys(nestedDict))
1191 keys.Add($
"{key}.{nestedKey}");
1204 internal static object ProcessDictionaryValue(
object value, ConverterSettings converterSettings, List<List<string>> gridRows =
null, HashSet<string> gridColumns =
null,
string parentKey =
null, HashSet<object> visited =
null)
1208 if (gridRows !=
null && gridColumns ==
null)
1210 gridRows.Add(
new List<string> {
"" });
1215 Type valueType = value.GetType();
1217 if (visited ==
null)
1218 visited =
new HashSet<object>(ConverterHelper.ReferenceEqualityComparer.Instance);
1221 if (value is IDictionary dynamicDict)
1226 if (!visited.Add(value))
1227 return $
"Recursion to [{parentKey ?? valueType.Name}]";
1231 var resultDict =
new Dictionary<dynamic, dynamic>();
1232 var row =
new List<string>();
1234 if (gridColumns !=
null && gridRows !=
null)
1236 row =
new List<string>(
new string[gridColumns.Count]);
1238 foreach (
string col
in gridColumns)
1240 string cellValue =
"";
1241 if (col.Contains(
"."))
1243 var keyParts = col.Split(
'.');
1244 object current = dynamicDict;
1246 foreach (var part
in keyParts)
1248 if (current is IDictionary currentDict && currentDict.Contains(part))
1250 current = currentDict[part];
1260 var processedValue = ProcessDictionaryValue(current, converterSettings,
null,
null,
null, visited);
1261 cellValue = FlattenValueForGrid(processedValue);
1264 else if (dynamicDict.Contains(col))
1266 var processedValue = ProcessDictionaryValue(dynamicDict[col], converterSettings,
null,
null,
null, visited);
1267 cellValue = FlattenValueForGrid(processedValue);
1269 row[colIdx++] = cellValue;
1274 foreach (dynamic entry
in dynamicDict)
1276 string key = entry.Key?.ToString();
1279 bool isTabularList = entry.Value is IList<object> list2 && list2.Count() > 0 && list2[0] is IDictionary;
1280 HashSet<string> nestedColumns = isTabularList ? CollectDictionaryKeys(entry.Value) : null;
1281 var processedValue = ProcessDictionaryValue(entry.Value, converterSettings, isTabularList ? gridRows :
null, nestedColumns, key, visited);
1282 resultDict[key] = processedValue;
1283 if (!isTabularList && gridRows !=
null && gridColumns ==
null)
1285 row.Add(FlattenValueForGrid(processedValue));
1290 if (gridRows !=
null && row.Count > 0 && gridColumns ==
null)
1298 visited.Remove(value);
1303 if (value is IList<object> list && !(value is
string))
1305 if (!visited.Add(value))
1306 return $
"Recursion to [{parentKey ?? valueType.Name}]";
1310 var resultList =
new List<object>();
1311 string itemKey =
string.Empty;
1313 if (
null != parentKey)
1314 itemKey = parentKey;
1318 foreach (var item
in list)
1320 var processedItem = ProcessDictionaryValue(item, converterSettings, gridRows, gridColumns, itemKey, visited);
1321 resultList.Add(processedItem);
1327 visited.Remove(value);
1332 if (ConverterHelper.IsSimpleType(valueType))
1335 if (valueType == typeof(Guid) && Guid.TryParse(value.ToString(), out var guid))
1337 stringValue = guid.ToString();
1338 if (gridRows !=
null && gridColumns ==
null)
1340 gridRows.Add(
new List<string> { stringValue });
1344 if (valueType == typeof(IPAddress) && IPAddress.TryParse(value.ToString(), out var ipAddress))
1346 stringValue = ipAddress.ToString();
1347 if (gridRows !=
null && gridColumns ==
null)
1349 gridRows.Add(
new List<string> { stringValue });
1353 if (valueType == typeof(TimeSpan) && TimeSpan.TryParse(value.ToString(), out var timeSpan))
1355 stringValue = timeSpan.ToString();
1356 if (gridRows !=
null && gridColumns ==
null)
1358 gridRows.Add(
new List<string> { stringValue });
1362 if (valueType == typeof(Uri))
1366 var uri =
new Uri(value.ToString());
1367 stringValue = uri.ToString();
1368 if (gridRows !=
null && gridColumns ==
null)
1370 gridRows.Add(
new List<string> { stringValue });
1376 stringValue = value.ToString();
1377 if (gridRows !=
null && gridColumns ==
null)
1379 gridRows.Add(
new List<string> { stringValue });
1384 if (valueType == typeof(System.Numerics.BigInteger))
1386 stringValue = value.ToString();
1387 if (gridRows !=
null && gridColumns ==
null)
1389 gridRows.Add(
new List<string> { stringValue });
1393 if (valueType == typeof(System.Version))
1395 stringValue = value.ToString();
1396 if (gridRows !=
null && gridColumns ==
null)
1398 gridRows.Add(
new List<string> { stringValue });
1402 stringValue = value.ToString();
1403 if (gridRows !=
null && gridColumns ==
null)
1405 gridRows.Add(
new List<string> { stringValue });
1411 if (ConverterHelper.IsClassType(valueType) && !valueType.IsArray)
1413 var resultDict = ConverterHelper.ConvertClassToDictionary(value);
1414 var row =
new List<string>();
1415 foreach (dynamic entry
in resultDict)
1417 row.Add(FlattenValueForGrid(entry.Value));
1419 if (gridRows !=
null && row.Count > 0 && gridColumns ==
null)
1427 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Unknown value type [{valueType}] detected. Converting to string.", value, GPALObjectType.Other);
1428 string fallbackValue = value.ToString();
1429 if (gridRows !=
null && gridColumns ==
null)
1431 gridRows.Add(
new List<string> { fallbackValue });
1433 return fallbackValue;
1436 private static string FlattenValueForGrid(
object value)
1443 if (value is IDictionary dynamicDict)
1445 var values =
new List<string>();
1446 foreach (dynamic entry
in dynamicDict)
1448 values.Add(FlattenValueForGrid(entry.Value));
1450 return string.Join(
",", values);
1453 if (value is IEnumerable<object> enumerable && !(value is
string))
1455 var values =
new List<string>();
1456 foreach (var item
in enumerable)
1458 values.Add(FlattenValueForGrid(item));
1460 return string.Join(
",", values);
1463 return value.ToString();
1466 internal static bool TokenizeFile(GPALFile inputFile, ConverterSettings myConverterSettings =
null)
1468 var xmlDoc =
new XmlDocument();
1470 StreamReader input =
null;
1471 IGPALGrid<string> outGrid =
null;
1472 List<IGPALGrid<string>> tokens =
new List<IGPALGrid<string>>(); ;
1473 bool retval =
false;
1483 bool callerFirstLineIsColumnHeaders = myConverterSettings?.InputFirstLineIsColumnHeaders ??
false;
1489 DataFormat outDataFormat = myConverterSettings?.OutDataFormat ?? DataFormat.NOTSET;
1490 bool outputNeedsDictionary = outDataFormat == DataFormat.JSON
1491 || outDataFormat == DataFormat.XML
1492 || outDataFormat == DataFormat.YAML
1493 || outDataFormat == DataFormat.CLASS
1494 || outDataFormat == DataFormat.DICTIONARY
1495 || outDataFormat == DataFormat.DATABASE;
1497 ((IGPALFileInternal)inputFile).FileDictionary.Clear();
1498 foreach (
string filename
in inputFile.
Filenames)
1500 DataFormat inDataFormat = ConverterHelper.GetDataFormatFromExtension(filename);
1501 bool inputIsDelimited = ConverterHelper.IsDelimitedDataFormat(inDataFormat);
1503 myConverterSettings =
new ConverterSettings
1505 InDelimiter = ((IGPALFileInternal)inputFile).Delimiter.Count() > fileIdx && ((IGPALFileInternal)inputFile).Delimiter[fileIdx].HasValue
1506 ? ((IGPALFileInternal)inputFile).Delimiter[fileIdx].Value
1508 FieldsEnclosedInQuotes = ((IGPALFileInternal)inputFile).FieldsEnclosedInQuotes.Count() > fileIdx
1509 ? ((IGPALFileInternal)inputFile).FieldsEnclosedInQuotes[fileIdx]
1511 FirstLineIsColumnHeaders = ((IGPALFileInternal)inputFile).FirstLineIsColumnNames.Count() > fileIdx
1512 ? ((IGPALFileInternal)inputFile).FirstLineIsColumnNames[fileIdx]
1513 : callerFirstLineIsColumnHeaders,
1514 IgnoreFirstLineColumnHeaders = ((IGPALFileInternal)inputFile).IgnoreFirstLineColumnNames.Count() > fileIdx
1515 ? ((IGPALFileInternal)inputFile).IgnoreFirstLineColumnNames[fileIdx]
1517 ColumnNames = inputIsDelimited ? ((IGPALFileInternal)inputFile).FileSettings.ColumnList : new GPALGrid<string>(),
1518 ExcelSheetNames = ((IGPALFileInternal)inputFile).FileSettings.ExcelSheetNames,
1519 ExcelRowsPerSheet = ((IGPALFileInternal)inputFile).FileSettings.ExcelRowsPerSheet,
1520 InputFile = inputFile,
1522 if (fileIdx < ((IGPALFileInternal)inputFile).FileSettings.Delimiter.Count())
1524 ((IGPALFileInternal)inputFile).FileSettings.Delimiter[fileIdx] = ConverterHelper.GetDelimiterFromFormat(inDataFormat);
1528 ((IGPALFileInternal)inputFile).FileSettings.Delimiter.Add(ConverterHelper.GetDelimiterFromFormat(inDataFormat));
1533 string directory = Path.GetDirectoryName(filename);
1534 string filenamePart = Path.GetFileName(filename);
1535 foreach (string filename2 in Directory.GetFiles(string.IsNullOrEmpty(directory) ?
"." : directory, filenamePart))
1537 using (input = new StreamReader(filename2))
1539 input.BaseStream.Position = 0;
1541 switch (inDataFormat)
1543 case DataFormat.CARET:
1544 case DataFormat.CSV:
1545 case DataFormat.PIPE:
1546 case DataFormat.PRN:
1547 case DataFormat.TAB:
1548 var dictionary = ConverterHelper.ConvertDelimitedToDictionary(myConverterSettings, input, filename2);
1549 ((IGPALFileInternal)inputFile).FileDictionary.Add(dictionary);
1550 myConverterSettings.InputDictionary = ((IGPALFileInternal)inputFile).FileDictionary;
1552 if (outputNeedsDictionary)
1556 ((IGPALFileInternal)inputFile).FileData.Add(string.Empty);
1557 ((IGPALFileInternal)inputFile).FileGrid.Add(GPAL.Grid.ToGPALObject());
1561 dynamic delimitedData = ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(myConverterSettings, out outGrid);
1562 ((IGPALFileInternal)inputFile).FileData.Add(delimitedData);
1563 ((IGPALFileInternal)inputFile).FileGrid.Add(outGrid);
1571 case DataFormat.JSON:
1572 string fileString = input.ReadToEnd();
1573 input.BaseStream.Position = 0;
1575 dynamic jsonObject = JsonConvert.DeserializeObject<dynamic>(fileString, new JsonConverter[] { new CustomJsonConverter() });
1577 ((IGPALFileInternal)inputFile).FileData.Add(fileString);
1579 var records = new Dictionary<object, dynamic>();
1580 var gridRows = new List<List<string>>();
1583 dynamic ConvertArrayToSurrogateDict(IEnumerable<dynamic> array, int startIndex = 0)
1585 var surrogateDict = new Dictionary<object, dynamic>();
1586 int idx = startIndex;
1587 foreach (dynamic item in array)
1589 surrogateDict[$
"GPALKEY{idx:D4}"] = item;
1592 return surrogateDict;
1596 dynamic ReplaceArrays(dynamic value)
1598 if (value is IEnumerable<dynamic> enumerable && !(value is string))
1600 var items = Enumerable.ToList<dynamic>(enumerable);
1615 bool looksLikePairsArray = items.Count > 0 && items.All(it =>
1616 it is IDictionary d && d.Count == 2
1617 && d.Contains(
"Key") && d.Contains(
"Value"));
1618 if (looksLikePairsArray)
1620 var pairsDict = new Dictionary<object, dynamic>();
1621 foreach (IDictionary pairObj in items)
1623 dynamic pairKey = ReplaceArrays(pairObj[
"Key"]);
1624 dynamic pairValue = ReplaceArrays(pairObj[
"Value"]);
1625 pairsDict[pairKey] = pairValue;
1631 return ConvertArrayToSurrogateDict(items);
1633 else if (value is IDictionary<string, dynamic> dict)
1636 var newDict = new Dictionary<object, dynamic>();
1637 foreach (var kvp in dict)
1639 newDict[kvp.Key] = ReplaceArrays(kvp.Value);
1650 dynamic processedRoot = ReplaceArrays(jsonObject);
1653 if (processedRoot is IDictionary && ConverterHelper.IsArrayLike((IDictionary)processedRoot))
1655 records = (Dictionary<object, dynamic>)processedRoot;
1660 records[
"root"] = processedRoot;
1664 foreach (KeyValuePair<object, dynamic> pair in records.OrderBy(p => p.Key.ToString()))
1666 if (pair.Value is IDictionary rowDict)
1668 var row = new List<string>();
1669 foreach (dynamic kvp in rowDict)
1671 if (kvp.Key != null)
1673 string key = kvp.Key.ToString();
1674 if (key.StartsWith(
"GPALKEY")) continue;
1675 var processedValue = ProcessDictionaryValue(kvp.Value, myConverterSettings, null, null, key);
1676 if (!(kvp.Value is IDictionary) && !(kvp.Value is IList<object>))
1678 row.Add(FlattenValueForGrid(processedValue));
1689 ((IGPALFileInternal)inputFile).FileDictionary.Add(records);
1691 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"JSON FileDictionary count: [{records.Count}]", null, GPALObjectType.Other);
1693 var gridRow = GPAL.Grid.ToGPALObject();
1694 foreach (var row in gridRows)
1696 gridRow.AddRow(row);
1699 myConverterSettings.InputDictionary = ((IGPALFileInternal)inputFile).FileDictionary;
1700 ((IGPALFileInternal)inputFile).FileGrid.Add(gridRow);
1703 case DataFormat.PDF:
1706 case DataFormat.XLSX:
1707 IGPALGrid<string> grid = ExcelHelper.TokenizeExcelFile(filename2);
1708 List<string> colNames = new List<string>();
1709 List<Dictionary<object, dynamic>> returnList = new List<Dictionary<object, dynamic>>();
1711 ((IGPALFileInternal)inputFile).FileGrid.Add((IGPALGrid<string>)grid.Clone());
1714 using (var workbook = new XLWorkbook(filename))
1716 List<int> rowsPerSheet = new List<int>();
1717 List<string> sheetName = new List<string>();
1719 foreach (IXLWorksheet worksheet in workbook.Worksheets)
1721 int lastRowNumber = worksheet.LastRowUsed()?.RowNumber() ?? 0;
1723 int totalRows = Math.Max(lastRowNumber, worksheet.RangeUsed()?.LastRow().RowNumber() ?? 0);
1725 totalRows = worksheet.Rows().LastOrDefault()?.RowNumber() ?? 0;
1727 rowsPerSheet.Add(totalRows);
1728 sheetName.Add(worksheet.Name);
1731 ((IGPALFileInternal)inputFile).FileSettings.ExcelRowsPerSheet.AddRow(rowsPerSheet);
1732 ((IGPALFileInternal)inputFile).FileSettings.ExcelSheetNames.AddRow(sheetName);
1734 int columnHeaders = 0;
1735 foreach (int rowCnt in rowsPerSheet)
1737 if (myConverterSettings.FirstLineIsColumnHeaders)
1739 colNames = grid[columnHeaders];
1744 foreach (string columnName in grid[columnHeaders].Select(v => (string)v))
1746 colNames.Add($
"Column{colIdx++}");
1750 myConverterSettings.ColumnNames.AddRow(colNames);
1752 if (0 == ((IGPALFileInternal)inputFile).FileSettings.ColumnList.Count())
1753 ((IGPALFileInternal)inputFile).FileSettings.ColumnList.AddRow(colNames);
1757 foreach (Dictionary<object, dynamic> row in ConverterHelper.ConvertGridToDictionary(colNames, grid, columnHeaders, rowCnt))
1759 if (true == myConverterSettings.FirstLineIsColumnHeaders && 0 == rowIdx++)
1762 returnList.Add(row);
1765 columnHeaders += rowCnt;
1771 foreach (int rowCnt in rowsPerSheet)
1773 if (myConverterSettings.FirstLineIsColumnHeaders)
1777 columnHeaders = rowCnt - iteration++;
1781 ((IGPALFileInternal)inputFile).FileDictionary = returnList;
1782 myConverterSettings.InputDictionary = ((IGPALFileInternal)inputFile).FileDictionary;
1785 case DataFormat.XML:
1786 xmlDoc.PreserveWhitespace = true;
1788 input.BaseStream.Position = 0;
1789 ((IGPALFileInternal)inputFile).FileDictionary.Add(ConverterHelper.ConvertXMLToDictionary(xmlDoc));
1790 myConverterSettings.InputDictionary = ((IGPALFileInternal)inputFile).FileDictionary;
1791 ((IGPALFileInternal)inputFile).FileData.Add(ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(myConverterSettings, out outGrid));
1792 ((IGPALFileInternal)inputFile).FileGrid.Add(outGrid);
1795 case DataFormat.YAML:
1796 var deserializer = new DeserializerBuilder()
1797 .WithNamingConvention(CamelCaseNamingConvention.Instance)
1800 string yamlText = input.ReadToEnd();
1801 input.BaseStream.Position = 0;
1802 dynamic yamlObject = deserializer.Deserialize<object>(yamlText);
1803 ((IGPALFileInternal)inputFile).FileData.Add(yamlText);
1805 ((IGPALFileInternal)inputFile).FileDictionary.Clear();
1806 ((IGPALFileInternal)inputFile).FileGrid.Clear();
1808 var yamlGridRows = new List<List<string>>();
1809 var processedYamlDict = new Dictionary<dynamic, dynamic>();
1811 if (yamlObject is IDictionary yamlMapping)
1813 foreach (var key in yamlMapping.Keys)
1815 dynamic processedKey = (key is IDictionary || key is IList)
1816 ? ProcessDictionaryValue(key, myConverterSettings, yamlGridRows)
1817 : (key?.ToString() ??
"null_key");
1818 processedYamlDict[processedKey] = ProcessDictionaryValue(yamlMapping[key], myConverterSettings, yamlGridRows);
1821 else if (yamlObject is IList yamlSequence)
1826 foreach (var item in yamlSequence)
1828 processedYamlDict[$
"GPALKEY{rootIdx++:D4}"] = ProcessDictionaryValue(item, myConverterSettings, yamlGridRows);
1834 processedYamlDict[
"GPALKEY0000"] = ProcessDictionaryValue(yamlObject, myConverterSettings, yamlGridRows);
1837 var yamlGridRow = GPAL.Grid.ToGPALObject();
1838 foreach (var row in yamlGridRows)
1840 yamlGridRow.AddRow(row);
1843 ((IGPALFileInternal)inputFile).FileDictionary.Add(processedYamlDict);
1844 myConverterSettings.InputDictionary = ((IGPALFileInternal)inputFile).FileDictionary;
1845 ((IGPALFileInternal)inputFile).FileGrid.Add(yamlGridRow);
1848 case DataFormat.HTML:
1849 ((IGPALFileInternal)inputFile).FileDictionary = ConverterHelper.ConvertHtmlToDictionary(filename2);
1850 myConverterSettings.InputDictionary = ((IGPALFileInternal)inputFile).FileDictionary;
1851 dynamic inData = ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(myConverterSettings, out outGrid);
1852 ((IGPALFileInternal)inputFile).FileData.Add(inData);
1853 ((IGPALFileInternal)inputFile).FileGrid.Add(outGrid);
1861 catch (Exception ex)
1863 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Problem tokenizing file [{filename}]. Continuing.", inputFile, GPALObjectType.GPALFile, ex);
1868 if (0 == ((IGPALFileInternal)inputFile).FileSettings.FileGrid.Count)
1869 FileHelper.GetGrid(((IGPALFileInternal)inputFile).FileSettings, out tokens);
1871 tokens = ((IGPALFileInternal)inputFile).FileSettings.FileGrid;
1873 ((IGPALFileInternal)inputFile).TokenList = tokens;
1874 ((IGPALFileInternal)inputFile).AlreadyTokenized = retval;
1878 [DllImport(
"kernel32.dll", CharSet = CharSet.Unicode, SetLastError =
true)]
1879 private static extern int GetShortPathName(
string lpszLongPath, StringBuilder lpszShortPath,
int cchBuffer);
1890 var sb =
new StringBuilder(1024);
1891 int result = GetShortPathName(longPath, sb, sb.Capacity);
1892 return result > 0 ? sb.ToString() : longPath;
1901 internal static bool IsWebAddress(
string fileName)
1903 return true == Uri.TryCreate(fileName, UriKind.Absolute, out Uri uri)
1904 && (Uri.UriSchemeHttp == uri.Scheme || Uri.UriSchemeHttps == uri.Scheme);
1919 internal static bool FetchOrDownloadInTab(
string url,
string localPath,
IBrowser browser)
1921 bool gotIt = FetchToFile(url, localPath, browser);
1924 gotIt = DownloadInTab(url, localPath, browser);
1957 internal static bool HasStartedDownloading(
string downloadDirectory,
string localPath)
1959 return true == File.Exists(localPath)
1960 || (
true == Directory.Exists(downloadDirectory)
1961 &&
true == Directory.EnumerateFiles(downloadDirectory).Any(file => file.EndsWith(
".crdownload") || file.EndsWith(
".part")));
1983 internal static int AllowanceFor(
string downloadDirectory, Browser.BrowserSettings browserSettings)
1985 int retVal =
true == IsFinalizing(downloadDirectory, watchArmed)
1986 ? browserSettings.DownloadTimeoutInSec * FinalizeBudgetMultiplier
1987 : browserSettings.DownloadTimeoutInSec;
2003 internal static void DiscardAbandonedDownload(Browser.BrowserSettings browserSettings)
2005 foreach (
string scratch
in watchedScratchFiles)
2008 if (
true == File.Exists(scratch))
2010 File.Delete(scratch);
2011 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{Path.GetFileName(scratch)}] was left behind by a download that never finished, removed so it does not name the next one around it", browserSettings.Browser, GPALObjectType.Browser);
2014 catch (Exception ex)
2016 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Could not remove [{scratch}]", browserSettings.Browser, GPALObjectType.Browser, ex);
2019 watchedScratchFiles.Clear();
2022 internal const int FinalizeBudgetMultiplier = 6;
2035 internal static bool IsFinalizing(
string downloadDirectory, DateTime since)
2037 bool retVal =
false;
2041 if (
true == Directory.Exists(downloadDirectory))
2047 retVal = Directory.EnumerateFiles(downloadDirectory)
2048 .Any(file =>
true == IsTemporaryDownload(file)
2049 && (File.GetCreationTime(file) >= since || File.GetLastWriteTime(file) >= since));
2051 catch (Exception ex)
2053 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Could not look for a download being finished in [{downloadDirectory}]",
null, GPALObjectType.None, ex);
2059 internal static long DownloadProgress(
string downloadDirectory,
string localPath)
2065 if (
true == File.Exists(localPath))
2066 retVal =
new FileInfo(localPath).Length;
2068 if (
true == Directory.Exists(downloadDirectory))
2069 foreach (
string scratch
in Directory.EnumerateFiles(downloadDirectory).Where(f =>
true == IsTemporaryDownload(f)))
2070 retVal = (0 > retVal ? 0 : retVal) +
new FileInfo(scratch).Length;
2072 catch (Exception ex)
2074 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Could not measure the download in [{downloadDirectory}]",
null, GPALObjectType.None, ex);
2079 internal static bool DownloadInTab(
string url,
string localPath, IBrowser browser)
2081 Browser.BrowserSettings browserSettings = ((Browser.Browser)browser).BrowserSettings;
2083 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Downloading [{url}] with the browser to [{localPath}].", browser, GPALObjectType.Browser);
2089 string landsAt = Path.Combine(downloadDirectory, Path.GetFileName(localPath));
2093 if (
true == browserSettings.UseOttoMagic)
2094 browserSettings.MagicHelper.WithDownloadFile(Path.GetFileName(localPath));
2095 else if (
true == browserSettings.UsePuppeteer)
2096 browserSettings.PuppeteerClient.DownloadTo(downloadDirectory).Execute();
2098 BrowserHelper.SetHeadlessDownload(browserSettings, landsAt);
2102 browserSettings.FileDownloaded =
null;
2103 SetupDownloadWatcher(landsAt, browserSettings);
2105 browser.NewTab(url);
2107 Stopwatch waitingToStart = Stopwatch.StartNew();
2112 long lastSeen =
long.MinValue;
2114 while (
null == browserSettings.FileDownloaded ||
false == browserSettings.FileDownloaded)
2116 long progress = DownloadProgress(downloadDirectory, landsAt);
2118 if (progress != lastSeen)
2120 lastSeen = progress;
2121 waitingToStart.Restart();
2123 else if (waitingToStart.Elapsed.TotalSeconds > AllowanceFor(downloadDirectory, browserSettings))
2132 bool downloaded =
true == browserSettings.FileDownloaded;
2136 if (
true == downloaded &&
false == landsAt.Equals(localPath, StringComparison.OrdinalIgnoreCase))
2138 if (
true == File.Exists(localPath))
2139 File.Delete(localPath);
2141 File.Move(landsAt, localPath);
2142 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{landsAt}] moved to [{localPath}]", browser, GPALObjectType.Browser);
2145 if (
false == downloaded)
2146 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"[{url}] did not download to [{downloadDirectory}] within [{browserSettings.DownloadTimeoutInSec}] seconds.", browser, GPALObjectType.Browser);
2163 internal static bool FetchToFile(
string url,
string localPath, IBrowser browser)
2165 bool downloaded =
false;
2166 string[] headers =
true == File.Exists(localPath)
2167 ?
new string[] {
"If-Modified-Since", File.GetLastWriteTimeUtc(localPath).ToString(
"R") }
2170 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Downloading [{url}] with the browser to [{localPath}].", browser, GPALObjectType.Browser);
2172 byte[] bytes = BrowserHelper.FetchBytes((Browser.Browser)browser, url, headers, out
string lastModified);
2174 if (304 == browser.ServerResponseCode)
2176 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{url}] has not changed, using [{localPath}].", browser, GPALObjectType.Browser);
2179 else if (
null == bytes)
2180 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"[{url}] returned no content to save as [{localPath}], HTTP [{browser.ServerResponseCode}].", browser, GPALObjectType.Browser);
2181 else if (200 > browser.ServerResponseCode || 300 <= browser.ServerResponseCode)
2184 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"[{url}] returned HTTP [{browser.ServerResponseCode}], nothing saved to [{localPath}].", browser, GPALObjectType.Browser);
2185 else if (0 == bytes.Length)
2186 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"[{url}] returned an empty file, nothing saved to [{localPath}].", browser, GPALObjectType.Browser);
2189 File.WriteAllBytes(localPath, bytes);
2193 if (
true == DateTime.TryParse(lastModified, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AdjustToUniversal, out DateTime modified))
2194 File.SetLastWriteTimeUtc(localPath, modified);
2211 if (!directoryPath.EndsWith(
"\\"))
2214 directoryPath +=
"\\";
2216 return directoryPath;
2224 List<Dictionary<object, dynamic>> output =
new List<Dictionary<object, dynamic>>();
2226 ((IGPALFileInternal)inputFile).FileDictionary.Clear();
2228 foreach (
string filename
in inputFile.
Filenames)
2234 InDelimiter = (0 < ((IGPALFileInternal)inputFile).Delimiter.Count() && ((IGPALFileInternal)inputFile).Delimiter[fileIdx].HasValue) ? ((IGPALFileInternal)inputFile).Delimiter[fileIdx].Value : (char?)null,
2235 FieldsEnclosedInQuotes = 0 < ((IGPALFileInternal)inputFile).FieldsEnclosedInQuotes.Count() && ((IGPALFileInternal)inputFile).FieldsEnclosedInQuotes[fileIdx],
2236 FirstLineIsColumnHeaders = 0 < ((IGPALFileInternal)inputFile).FirstLineIsColumnNames.Count() && ((IGPALFileInternal)inputFile).FirstLineIsColumnNames[fileIdx],
2239 DataFormat InDataFormat =
ConverterHelper.GetDataFormatFromExtension(filename);
2241 string[] csvLines = File.ReadAllLines(filename);
2244 string[] headers = csvLines[0].Trim().Split(
',');
2245 List<dynamic[]> dataLines =
new List<dynamic[]>();
2248 for (
int i = 1; i < csvLines.Length; i++)
2250 string[] data = csvLines[i].Trim().Split(
',');
2251 dataLines.Add(data);
2255 foreach (var data
in dataLines)
2257 Dictionary<object, dynamic> rowData =
new Dictionary<object, dynamic>();
2259 for (
int i = 0; i < headers.Length; i++)
2261 string[] headerParts = headers[i].Split(
'.');
2262 Dictionary<object, dynamic> currentDict = rowData;
2264 for (
int j = 0; j < headerParts.Length; j++)
2266 string part = headerParts[j];
2267 bool isLastPart = (j == headerParts.Length - 1);
2269 if (part.EndsWith(
"]"))
2271 int indexStart = part.LastIndexOf(
'[');
2272 string arrayName = part.Substring(0, indexStart);
2273 int arrayIndex =
int.Parse(part.Substring(indexStart + 1, part.Length - indexStart - 2));
2276 if (!currentDict.ContainsKey(arrayName))
2278 currentDict[arrayName] =
new List<Dictionary<string, object>>();
2282 List<Dictionary<object, dynamic>> currentArray = (List<Dictionary<object, dynamic>>)currentDict[arrayName];
2283 while (currentArray.Count <= arrayIndex)
2285 currentArray.Add(
new Dictionary<object, dynamic>());
2290 currentArray[arrayIndex] = data[i];
2294 currentDict = currentArray[arrayIndex];
2299 if (!currentDict.ContainsKey(part))
2301 currentDict[part] = isLastPart ? (object)data[i] : new Dictionary<object, dynamic>();
2306 currentDict[part] = data[i];
2310 currentDict = (Dictionary<object, dynamic>)currentDict[part];
2317 output.Add(rowData);
2320 catch (Exception ex)
2322 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to open file [{filename}]. Continuing.", inputFile, GPALObjectType.GPALFile, ex);
2327 ((IGPALFileInternal)inputFile).FileDictionary = output;
2331 internal static Dictionary<object, dynamic> ConvertToStringDynamicDict(dynamic inputDict)
2333 var resultDict =
new Dictionary<object, dynamic>();
2334 foreach (var kvp
in inputDict)
2336 if (kvp.Key !=
null)
2338 string key = kvp.Key.ToString();
2339 object value = kvp.Value;
2341 if (value is IDictionary nestedDict)
2344 resultDict.Add(key, ConvertToStringDynamicDict(nestedDict));
2349 resultDict.Add(key, value);