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");
294 internal static bool DownloadToFile(
string filename,
string downloadUrl)
296 bool downloaded =
false;
297 string url = downloadUrl;
301 using (System.Net.Http.HttpClient client =
new System.Net.Http.HttpClient())
303 System.Net.Http.HttpRequestMessage request =
new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, url);
309 request.Headers.TryAddWithoutValidation(
"User-Agent",
310 true ==
string.IsNullOrWhiteSpace(
GPAL.GPALSettings.UserAgent)
312 :
GPAL.GPALSettings.UserAgent);
314 if (
true == File.Exists(filename))
315 request.Headers.IfModifiedSince = File.GetLastWriteTimeUtc(filename);
317 System.Net.Http.HttpResponseMessage response = client.SendAsync(request).GetAwaiter().GetResult();
319 if (System.Net.HttpStatusCode.NotModified == response.StatusCode)
321 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{url}] has not changed, using [{filename}].",
null, GPALObjectType.None);
324 else if (
false == response.IsSuccessStatusCode)
328 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{url}] returned HTTP [{(int)response.StatusCode}]. Give the file a browser and it is fetched on that browser's session instead",
null, GPALObjectType.None);
332 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Downloading [{url}] to [{filename}].",
null, GPALObjectType.None);
333 File.WriteAllBytes(filename, response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult());
337 if (
null != response.Content.Headers.LastModified)
338 File.SetLastWriteTimeUtc(filename, response.Content.Headers.LastModified.Value.UtcDateTime);
349 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{url}] failed to save to [{filename}].",
null, GPALObjectType.None, ex);
359 internal static string GetDownloadUrl(
GPALElement element)
362 string downloadUrl =
null;
365 if (
"GPALElement" == element.
TagName)
367 downloadUrl = ElementHelper.UrlUnderImage(element);
370 else if (element.
TagName.ToLower() ==
"a")
375 else if (element.
TagName.ToLower() ==
"img")
380 else if (element.
TagName.ToLower() ==
"script")
385 else if (element.
TagName.ToLower() ==
"link")
390 else if (element.
TagName.ToLower() ==
"object")
395 else if (element.
TagName.ToLower() ==
"embed")
400 else if (element.
TagName.ToLower() ==
"frame" || element.
TagName.ToLower() ==
"iframe")
405 else if (element.
TagName.ToLower() ==
"audio")
410 else if (element.
TagName.ToLower() ==
"video")
418 internal static FileSystemWatcher watcher;
419 internal static DateTime watchArmed;
420 internal static readonly HashSet<string> watchedScratchFiles =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
422 internal static string SetupDownloadWatcher(
string fullFilePathToSave,
Browser.
BrowserSettings browserSettings)
428 bool downloadsWhereItLikes =
true == browserSettings.UseOttoMagic
429 || BrowserType.FireFox == browserSettings.BrowserType;
430 string downloadDirectory =
true == downloadsWhereItLikes
434 string fileName = Path.GetFileName(fullFilePathToSave);
435 string actualDownloadedFilename =
null;
437 watchArmed = DateTime.Now;
438 watchedScratchFiles.Clear();
440 watcher =
new FileSystemWatcher(downloadDirectory);
441 watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.Attributes;
442 watcher.Filter =
"*.*";
443 watcher.EnableRaisingEvents =
true;
446 watcher.Created += (sender, e) => handleFile(sender, e, downloadDirectory, fullFilePathToSave);
447 watcher.Changed += (sender, e) => handleFile(sender, e, downloadDirectory, fullFilePathToSave);
448 watcher.Renamed += (sender, e) => handleFile(sender, e, downloadDirectory, fullFilePathToSave);
450 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Watching [{downloadDirectory}] for [{fileName}] w/timeout [{browserSettings.DownloadTimeoutInSec}] secs",
null, GPALObjectType.None);
451 System.Threading.Thread.Sleep(1_000);
453 return downloadDirectory;
455 void handleFile(
object sender, FileSystemEventArgs e,
string downloaddirectory,
string fullFilePathToSave)
462 if (e is RenamedEventArgs renamed
463 && (
true == renamed.OldName.EndsWith(
".crdownload") ||
true == renamed.OldName.EndsWith(
".part")))
465 actualDownloadedFilename = WaitForDownloadToFinish(Path.Combine(downloaddirectory, renamed.Name), browserSettings.DownloadTimeoutInSec);
466 browserSettings.FileDownloaded =
false ==
string.IsNullOrEmpty(actualDownloadedFilename);
468 if (
true == browserSettings.FileDownloaded)
470 File.Move(actualDownloadedFilename, fullFilePathToSave);
471 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{actualDownloadedFilename}] moved to [{fullFilePathToSave}]",
null, GPALObjectType.None);
474 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{renamed.OldName}] was renamed to [{renamed.Name}] but it never settled",
null, GPALObjectType.None);
478 if (
true == browserSettings.FileDownloaded)
479 ((FileSystemWatcher)sender).Dispose();
484 if (e.Name.EndsWith(
".crdownload") || e.Name.EndsWith(
".part"))
486 string filename = e.Name.Replace(
".crdownload",
"").Replace(
".part",
"");
487 string fullSavedFilePath = Path.Combine(downloaddirectory, filename);
489 string pattern =
@"^downloads(?:\.htm|\s*\(\d+\)\.htm|\.htm\s*\(\d+\))$";
490 if (Regex.IsMatch(filename, pattern, RegexOptions.IgnoreCase))
495 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Download started for [{e.Name}] waiting on expected [{fullSavedFilePath}]",
null, GPALObjectType.None);
496 watchedScratchFiles.Add(Path.Combine(downloaddirectory, e.Name));
502 if (
true == File.Exists(Path.Combine(downloaddirectory, e.Name)))
503 actualDownloadedFilename = WaitForDownloadToFinish(Path.Combine(downloaddirectory, e.Name), browserSettings.DownloadTimeoutInSec);
509 if (
true == IsTemporaryDownload(actualDownloadedFilename))
510 actualDownloadedFilename =
null;
514 if (
true == File.Exists(fullFilePathToSave))
515 actualDownloadedFilename = WaitForDownloadToFinish(fullFilePathToSave, browserSettings.DownloadTimeoutInSec);
519 if (
true ==
string.IsNullOrEmpty(actualDownloadedFilename))
520 actualDownloadedFilename = WaitForDownloadToFinish(fullSavedFilePath, browserSettings.DownloadTimeoutInSec);
522 browserSettings.FileDownloaded = !
string.IsNullOrEmpty(actualDownloadedFilename);
524 if (
true == browserSettings.FileDownloaded)
528 if (
true == fullFilePathToSave.Equals(actualDownloadedFilename, StringComparison.OrdinalIgnoreCase))
529 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{fullFilePathToSave}] downloaded straight to where it was asked to go",
null, GPALObjectType.None);
532 File.Move(actualDownloadedFilename, fullFilePathToSave);
533 if (File.Exists(actualDownloadedFilename))
534 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Unable to move [{actualDownloadedFilename}] to [{fullFilePathToSave}]",
null, GPALObjectType.None);
536 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{actualDownloadedFilename}] moved to [{fullFilePathToSave}]",
null, GPALObjectType.None);
541 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"WaitForDownloadToFinish returned null for [{fullSavedFilePath}] - check timeout or stability",
null, GPALObjectType.None);
546 if (
true == browserSettings.FileDownloaded)
547 ((FileSystemWatcher)sender).Dispose();
556 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Error for [{fullFilePathToSave}]",
null, GPALObjectType.None, ex);
567 internal const int DownloadSizeSampleMs = 500;
574 internal static bool IsTemporaryDownload(
string path)
576 bool retVal =
true == path?.EndsWith(
".crdownload") ||
true == path?.EndsWith(
".part");
593 internal static string NewestDownloadSince(
string directory, DateTime since)
595 string retVal =
null;
596 DateTime newest = DateTime.MinValue;
598 foreach (
string candidate
in Directory.EnumerateFiles(directory))
599 if (
false == IsTemporaryDownload(candidate))
605 DateTime written = File.GetLastWriteTime(candidate);
606 DateTime created = File.GetCreationTime(candidate);
607 DateTime arrived = created > written ? created : written;
609 if (arrived >= since && arrived > newest)
619 internal static string WaitForDownloadToFinish(
string filePath,
int downloadTimeoutInSec)
621 long size = 0, newSize = 0;
622 string downloadedFilename;
623 bool fileStarted =
false;
625 newSize = WaitForFileToDownload(filePath, downloadTimeoutInSec, size, out downloadedFilename);
631 System.Threading.Thread.Sleep(1_000);
632 else if (-1 == newSize)
634 if (
false == filePath.EndsWith(
".crdownload") &&
false == filePath.EndsWith(
".part"))
645 if (0 <= (newSize = WaitForFileToDownload(filePath, downloadTimeoutInSec, newSize, out downloadedFilename)))
654 System.Threading.Thread.Sleep(1_000);
659 System.Threading.Thread.Sleep(DownloadSizeSampleMs);
661 if (size == (newSize = WaitForFileToDownload(filePath, downloadTimeoutInSec, newSize, out downloadedFilename)))
666 else if (-1 == newSize)
670 if (
false == filePath.EndsWith(
".crdownload") &&
false == filePath.EndsWith(
".part"))
673 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"File [{downloadedFilename ?? filePath}] downloaded as zero bytes",
null, GPALObjectType.None);
674 else if (-1 == newSize &&
false == fileStarted)
675 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"File [{downloadedFilename ?? filePath}] NOT downloaded",
null, GPALObjectType.None);
679 return downloadedFilename;
681 internal static long WaitForFileToDownload(
string filePath,
int downloadTimeoutInSec,
long size, out
string downloadedFilename)
683 TimeSpan timeout = TimeSpan.FromSeconds(downloadTimeoutInSec);
684 DateTime startTime = DateTime.Now;
685 bool fileExists = Directory.GetFiles(Path.GetDirectoryName(filePath), Path.GetFileName(filePath)).Length > 0;
686 int lastPrinted = -1;
693 if (
false == fileExists &&
true == IsTemporaryDownload(filePath))
695 string arrived = NewestDownloadSince(Path.GetDirectoryName(filePath), watchArmed);
697 if (
true ==
string.IsNullOrEmpty(arrived))
699 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"[{filePath}] has gone and nothing arrived with it, so the download did not finish.",
null, GPALObjectType.None);
700 downloadedFilename = filePath;
705 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"[{Path.GetFileName(filePath)}] became [{Path.GetFileName(arrived)}], which is what the browser called it.",
null, GPALObjectType.None);
710 while (
false == fileExists && (DateTime.Now - startTime) < timeout)
712 TimeSpan remaining = timeout - (DateTime.Now - startTime);
713 int remainingSec = (int)Math.Ceiling(remaining.TotalSeconds);
716 if (remainingSec % 10 == 0 && remainingSec != lastPrinted)
717 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Waiting up tp [{remainingSec}] sec for [{filePath}].",
null, GPALObjectType.None);
719 System.Threading.Thread.Sleep(1000);
720 fileExists = Directory.GetFiles(Path.GetDirectoryName(filePath), Path.GetFileName(filePath)).Length > 0;
726 if (
false == fileExists)
728 string arrived = NewestDownloadSince(Path.GetDirectoryName(filePath), watchArmed);
730 if (
false ==
string.IsNullOrEmpty(arrived) &&
false == arrived.Equals(filePath, StringComparison.OrdinalIgnoreCase))
732 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);
739 if (
false == fileExists)
741 if (
false == filePath.EndsWith(
".crdownload") &&
false == filePath.EndsWith(
".part"))
743 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"File [{filePath}] never showed up.",
null, GPALObjectType.None);
744 downloadedFilename =
null;
747 downloadedFilename = filePath;
757 var cutoff = DateTime.Now.AddMinutes(-2);
759 string newest =
null;
760 DateTime newestTime = DateTime.MinValue;
761 IEnumerable<string> filesFound = Directory.EnumerateFiles(
762 Path.GetDirectoryName(filePath),
763 Path.GetFileName(filePath));
766 if (1 < filesFound.Count())
767 foreach (var f
in filesFound)
770 if (
true == f.Equals(
GPAL.GPALSettings.ChromeDriverZipFilename) ||
771 true == f.Equals(
GPAL.GPALSettings.EdgeDriverFilename) ||
772 true == f.Equals(
GPAL.GPALSettings.FirefoxDriverFilename))
775 var t = File.GetLastWriteTime(f);
777 if (t >= cutoff && t > newestTime)
784 newest = filesFound.FirstOrDefault();
792 downloadedFilename =
true == IsTemporaryDownload(filePath) ? filePath :
null;
797 if (
true == newest.Equals(
GPAL.GPALSettings.ChromeDriverZipFilename) ||
798 true == newest.Equals(
GPAL.GPALSettings.EdgeDriverFilename) ||
799 true == newest.Equals(
GPAL.GPALSettings.FirefoxDriverFilename))
805 downloadedFilename = newest;
810 using (FileStream fs = File.Open(downloadedFilename, FileMode.Open))
813 if (size != fs.Length)
814 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"File [{downloadedFilename}] downloaded with file size [{fs.Length}].",
null, GPALObjectType.None);
825 FileInfo fileInfo =
new FileInfo(downloadedFilename);
830 if (size != fileInfo.Length)
831 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"File [{downloadedFilename}] locked, using FileInfo size [{fileInfo.Length}].",
null, GPALObjectType.None);
833 return fileInfo.Length;
838 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"File [{downloadedFilename}] unable to determine file size.",
null, GPALObjectType.None, ex);
846 downloadedFilename =
null;
860 if (BrowserType.Chrome == browser.BrowserType)
862 else if (BrowserType.Edge == browser.BrowserType)
864 else if (BrowserType.FireFox == browser.BrowserType)
868 string defaultDownloadDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) +
@"\Downloads\";
869 return defaultDownloadDirectory;
880 if (
string.IsNullOrEmpty(profilePath))
882 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
883 profilePath = Path.Combine(localAppData,
@"Google\Chrome\User Data\Default");
886 string prefsPath =
null;
888 if (
false ==
new DirectoryInfo(profilePath).Name.ToLower().Equals(
"default"))
889 prefsPath = Path.Combine(profilePath,
@"default\Preferences");
891 prefsPath = Path.Combine(profilePath,
@"Preferences");
893 if (File.Exists(prefsPath))
895 string json = File.ReadAllText(prefsPath);
896 JObject prefs = JObject.Parse(json);
899 var downloadDir = prefs[
"download"]?[
"default_directory"]?.ToString();
901 if (!
string.IsNullOrEmpty(downloadDir))
905 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
908 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads"); ;
920 if (
string.IsNullOrEmpty(profilePath))
922 string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
923 profilePath = Path.Combine(localAppData,
@"Microsoft\Edge\User Data\Default");
926 string prefsPath = Path.Combine(profilePath,
@"default\Preferences");
928 if (File.Exists(prefsPath))
931 string json = File.ReadAllText(prefsPath);
932 JObject prefs = JObject.Parse(json);
940 var downloadDir = prefs[
"download"]?[
"default_directory"]?.ToString();
942 if (!
string.IsNullOrEmpty(downloadDir))
946 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
949 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
960 string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
961 string firefoxDir = Path.Combine(appData,
@"Mozilla\Firefox");
962 string profilesIniPath = Path.Combine(firefoxDir,
"profiles.ini");
964 if (!File.Exists(profilesIniPath))
968 string[] lines = File.ReadAllLines(profilesIniPath);
969 string profileDir =
null;
970 bool isRelative =
true;
972 for (
int i = 0; i < lines.Length; i++)
974 string line = lines[i].Trim();
976 if (line.StartsWith(
"[Profile", StringComparison.OrdinalIgnoreCase))
979 string currentPath =
null;
980 bool currentIsRelative =
true;
981 bool isDefault =
false;
984 for (
int j = i + 1; j < lines.Length; j++)
986 string subLine = lines[j].Trim();
987 if (subLine.StartsWith(
"["))
990 if (subLine.StartsWith(
"Path=", StringComparison.OrdinalIgnoreCase))
991 currentPath = subLine.Substring(5);
992 else if (subLine.StartsWith(
"IsRelative=", StringComparison.OrdinalIgnoreCase))
993 currentIsRelative = subLine.Substring(11) ==
"1";
994 else if (subLine.StartsWith(
"Default=", StringComparison.OrdinalIgnoreCase))
995 isDefault = subLine.Substring(8) ==
"1";
999 if (isDefault && currentPath !=
null)
1001 profileDir = currentPath;
1002 isRelative = currentIsRelative;
1007 if (profileDir ==
null && currentPath !=
null)
1009 profileDir = currentPath;
1010 isRelative = currentIsRelative;
1015 if (!
string.IsNullOrEmpty(profileDir))
1019 string fullProfilePath = isRelative
1020 ? Path.Combine(firefoxDir, profileDir)
1024 if (!
string.IsNullOrEmpty(profilePath))
1025 fullProfilePath = profilePath;
1027 string prefsJsPath = Path.Combine(fullProfilePath,
"prefs.js");
1029 if (!File.Exists(prefsJsPath))
1033 string content = File.ReadAllText(prefsJsPath);
1036 var match = Regex.Match(content,
@"user_pref\(""browser\.download\.dir"",\s*""([^""]*)""\);");
1040 string dir = match.Groups[1].Value;
1042 return dir.Replace(
"\\\\",
"\\");
1046 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
1050 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
1057 internal static List<IGPALGrid<string>> TokenizeFile(
UnitOfWork currentUOW,
GPALFile inputFile)
1059 currentUOW.InputFile = inputFile;
1060 int rowCount =
FileHelper.GetGrid(((IGPALFileInternal)inputFile).FileSettings, out List<IGPALGrid<string>> tokens);
1068 private static HashSet<string> CollectDictionaryKeys(
object value)
1070 var keys =
new HashSet<string>();
1071 if (value is IList<object> list)
1073 foreach (var item
in list)
1075 if (item is IDictionary dynamicDict)
1077 foreach (dynamic entry
in dynamicDict)
1079 string key = entry.Key?.ToString();
1082 if (entry.Value is IDictionary nestedDict)
1084 foreach (
string nestedKey
in CollectDictionaryKeys(nestedDict))
1086 keys.Add($
"{key}.{nestedKey}");
1098 else if (value is IDictionary dynamicDict)
1100 foreach (dynamic entry
in dynamicDict)
1102 string key = entry.Key?.ToString();
1105 if (entry.Value is IDictionary nestedDict)
1107 foreach (
string nestedKey
in CollectDictionaryKeys(nestedDict))
1109 keys.Add($
"{key}.{nestedKey}");
1124 internal static object ProcessDictionaryValue(
object value, List<List<string>> gridRows =
null, HashSet<string> gridColumns =
null,
string parentKey =
null, HashSet<object> visited =
null)
1128 if (gridRows !=
null && gridColumns ==
null)
1130 gridRows.Add(
new List<string> {
"" });
1135 Type valueType = value.GetType();
1137 if (visited ==
null)
1138 visited =
new HashSet<object>(ConverterHelper.ReferenceEqualityComparer.Instance);
1141 if (value is IDictionary dynamicDict)
1146 if (!visited.Add(value))
1147 return $
"Recursion to [{parentKey ?? valueType.Name}]";
1151 var resultDict =
new Dictionary<dynamic, dynamic>();
1152 var row =
new List<string>();
1154 if (gridColumns !=
null && gridRows !=
null)
1156 row =
new List<string>(
new string[gridColumns.Count]);
1158 foreach (
string col
in gridColumns)
1160 string cellValue =
"";
1161 if (col.Contains(
"."))
1163 var keyParts = col.Split(
'.');
1164 object current = dynamicDict;
1166 foreach (var part
in keyParts)
1168 if (current is IDictionary currentDict && currentDict.Contains(part))
1170 current = currentDict[part];
1180 var processedValue = ProcessDictionaryValue(current,
null,
null,
null, visited);
1181 cellValue = FlattenValueForGrid(processedValue);
1184 else if (dynamicDict.Contains(col))
1186 var processedValue = ProcessDictionaryValue(dynamicDict[col],
null,
null,
null, visited);
1187 cellValue = FlattenValueForGrid(processedValue);
1189 row[colIdx++] = cellValue;
1194 foreach (dynamic entry
in dynamicDict)
1196 string key = entry.Key?.ToString();
1199 bool isTabularList = entry.Value is IList<object> list2 && list2.Count() > 0 && list2[0] is IDictionary;
1200 HashSet<string> nestedColumns = isTabularList ? CollectDictionaryKeys(entry.Value) : null;
1201 var processedValue = ProcessDictionaryValue(entry.Value, isTabularList ? gridRows :
null, nestedColumns, key, visited);
1202 resultDict[key] = processedValue;
1203 if (!isTabularList && gridRows !=
null && gridColumns ==
null)
1205 row.Add(FlattenValueForGrid(processedValue));
1210 if (gridRows !=
null && row.Count > 0 && gridColumns ==
null)
1218 visited.Remove(value);
1223 if (value is IList<object> list && !(value is
string))
1225 if (!visited.Add(value))
1226 return $
"Recursion to [{parentKey ?? valueType.Name}]";
1230 var resultList =
new List<object>();
1231 string itemKey =
string.Empty;
1233 if (
null != parentKey)
1234 itemKey = parentKey;
1238 foreach (var item
in list)
1240 var processedItem = ProcessDictionaryValue(item, gridRows, gridColumns, itemKey, visited);
1241 resultList.Add(processedItem);
1247 visited.Remove(value);
1252 if (ConverterHelper.IsSimpleType(valueType))
1255 if (valueType == typeof(Guid) && Guid.TryParse(value.ToString(), out var guid))
1257 stringValue = guid.ToString();
1258 if (gridRows !=
null && gridColumns ==
null)
1260 gridRows.Add(
new List<string> { stringValue });
1264 if (valueType == typeof(IPAddress) && IPAddress.TryParse(value.ToString(), out var ipAddress))
1266 stringValue = ipAddress.ToString();
1267 if (gridRows !=
null && gridColumns ==
null)
1269 gridRows.Add(
new List<string> { stringValue });
1273 if (valueType == typeof(TimeSpan) && TimeSpan.TryParse(value.ToString(), out var timeSpan))
1275 stringValue = timeSpan.ToString();
1276 if (gridRows !=
null && gridColumns ==
null)
1278 gridRows.Add(
new List<string> { stringValue });
1282 if (valueType == typeof(Uri))
1286 var uri =
new Uri(value.ToString());
1287 stringValue = uri.ToString();
1288 if (gridRows !=
null && gridColumns ==
null)
1290 gridRows.Add(
new List<string> { stringValue });
1296 stringValue = value.ToString();
1297 if (gridRows !=
null && gridColumns ==
null)
1299 gridRows.Add(
new List<string> { stringValue });
1304 if (valueType == typeof(System.Numerics.BigInteger))
1306 stringValue = value.ToString();
1307 if (gridRows !=
null && gridColumns ==
null)
1309 gridRows.Add(
new List<string> { stringValue });
1313 if (valueType == typeof(System.Version))
1315 stringValue = value.ToString();
1316 if (gridRows !=
null && gridColumns ==
null)
1318 gridRows.Add(
new List<string> { stringValue });
1322 stringValue = value.ToString();
1323 if (gridRows !=
null && gridColumns ==
null)
1325 gridRows.Add(
new List<string> { stringValue });
1331 if (ConverterHelper.IsClassType(valueType) && !valueType.IsArray)
1333 var resultDict = ConverterHelper.ConvertClassToDictionary(value);
1334 var row =
new List<string>();
1335 foreach (dynamic entry
in resultDict)
1337 row.Add(FlattenValueForGrid(entry.Value));
1339 if (gridRows !=
null && row.Count > 0 && gridColumns ==
null)
1347 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Unknown value type [{valueType}] detected. Converting to string.", value, GPALObjectType.Other);
1348 string fallbackValue = value.ToString();
1349 if (gridRows !=
null && gridColumns ==
null)
1351 gridRows.Add(
new List<string> { fallbackValue });
1353 return fallbackValue;
1356 private static string FlattenValueForGrid(
object value)
1363 if (value is IDictionary dynamicDict)
1365 var values =
new List<string>();
1366 foreach (dynamic entry
in dynamicDict)
1368 values.Add(FlattenValueForGrid(entry.Value));
1370 return string.Join(
",", values);
1373 if (value is IEnumerable<object> enumerable && !(value is
string))
1375 var values =
new List<string>();
1376 foreach (var item
in enumerable)
1378 values.Add(FlattenValueForGrid(item));
1380 return string.Join(
",", values);
1383 return value.ToString();
1396 private static IGPALGrid<string> ColumnNamesForFile(GPALFile inputFile,
int fileIdx)
1398 IGPALGrid<string> columnList = ((IGPALFileInternal)inputFile).FileSettings.ColumnList;
1399 IGPALGrid<string> retVal = columnList;
1401 if (
false == ((IGPALFileInternal)inputFile).FileSettings.UseOneHeaderForAllFiles
1403 && fileIdx < columnList.Count())
1405 retVal = GPAL.GridForType<
string>();
1406 retVal.AddRow(
new List<string>(columnList[fileIdx]));
1412 internal static bool TokenizeFile(GPALFile inputFile, ConverterSettings myConverterSettings =
null)
1414 var xmlDoc =
new XmlDocument();
1416 StreamReader input =
null;
1417 IGPALGrid<string> outGrid =
null;
1418 List<IGPALGrid<string>> tokens =
new List<IGPALGrid<string>>(); ;
1419 bool retval =
false;
1429 bool callerFirstLineIsColumnHeaders = myConverterSettings?.InputFirstLineIsColumnHeaders ??
false;
1435 DataFormat outDataFormat = myConverterSettings?.OutDataFormat ?? DataFormat.NOTSET;
1436 bool outputNeedsDictionary = outDataFormat == DataFormat.JSON
1437 || outDataFormat == DataFormat.XML
1438 || outDataFormat == DataFormat.YAML
1439 || outDataFormat == DataFormat.CLASS
1440 || outDataFormat == DataFormat.DICTIONARY
1441 || outDataFormat == DataFormat.DATABASE;
1443 ((IGPALFileInternal)inputFile).FileDictionary.Clear();
1444 foreach (
string filename
in inputFile.
Filenames)
1446 DataFormat inDataFormat = ConverterHelper.GetDataFormatFromExtension(filename);
1447 bool inputIsDelimited = ConverterHelper.IsDelimitedDataFormat(inDataFormat);
1449 myConverterSettings =
new ConverterSettings
1451 InDelimiter = ((IGPALFileInternal)inputFile).Delimiter.Count() > fileIdx && ((IGPALFileInternal)inputFile).Delimiter[fileIdx].HasValue
1452 ? ((IGPALFileInternal)inputFile).Delimiter[fileIdx].Value
1454 FieldsEnclosedInQuotes = ((IGPALFileInternal)inputFile).FieldsEnclosedInQuotes.Count() > fileIdx
1455 ? ((IGPALFileInternal)inputFile).FieldsEnclosedInQuotes[fileIdx]
1457 FirstLineIsColumnHeaders = ((IGPALFileInternal)inputFile).FirstLineIsColumnNames.Count() > fileIdx
1458 ? ((IGPALFileInternal)inputFile).FirstLineIsColumnNames[fileIdx]
1459 : callerFirstLineIsColumnHeaders,
1460 IgnoreFirstLineColumnHeaders = ((IGPALFileInternal)inputFile).IgnoreFirstLineColumnNames.Count() > fileIdx
1461 ? ((IGPALFileInternal)inputFile).IgnoreFirstLineColumnNames[fileIdx]
1463 ColumnNames = inputIsDelimited ? ColumnNamesForFile(inputFile, fileIdx) : new GPALGrid<string>(),
1464 ExcelSheetNames = ((IGPALFileInternal)inputFile).FileSettings.ExcelSheetNames,
1465 ExcelRowsPerSheet = ((IGPALFileInternal)inputFile).FileSettings.ExcelRowsPerSheet,
1466 InputFile = inputFile,
1468 if (fileIdx < ((IGPALFileInternal)inputFile).FileSettings.Delimiter.Count())
1470 ((IGPALFileInternal)inputFile).FileSettings.Delimiter[fileIdx] = ConverterHelper.GetDelimiterFromFormat(inDataFormat);
1474 ((IGPALFileInternal)inputFile).FileSettings.Delimiter.Add(ConverterHelper.GetDelimiterFromFormat(inDataFormat));
1479 string directory = Path.GetDirectoryName(filename);
1480 string filenamePart = Path.GetFileName(filename);
1481 foreach (string filename2 in Directory.GetFiles(string.IsNullOrEmpty(directory) ?
"." : directory, filenamePart))
1483 using (input = new StreamReader(filename2))
1485 input.BaseStream.Position = 0;
1489 bool formatHandled = true;
1491 switch (inDataFormat)
1493 case DataFormat.CARET:
1494 case DataFormat.CSV:
1495 case DataFormat.PIPE:
1496 case DataFormat.PRN:
1497 case DataFormat.TAB:
1498 var dictionary = ConverterHelper.ConvertDelimitedToDictionary(myConverterSettings, input, filename2);
1499 ((IGPALFileInternal)inputFile).FileDictionary.Add(dictionary);
1504 myConverterSettings.InputDictionary = new List<Dictionary<object, dynamic>> { dictionary };
1506 if (outputNeedsDictionary)
1510 ((IGPALFileInternal)inputFile).FileData.Add(string.Empty);
1511 ((IGPALFileInternal)inputFile).FileGrid.Add(GPAL.Grid.ToGPALObject());
1515 dynamic delimitedData = ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(myConverterSettings, out outGrid);
1516 ((IGPALFileInternal)inputFile).FileData.Add(delimitedData);
1517 ((IGPALFileInternal)inputFile).FileGrid.Add(outGrid);
1525 case DataFormat.LOG:
1529 string logText = input.ReadToEnd();
1530 input.BaseStream.Position = 0;
1532 IGPALGrid<string> logGrid = GPAL.GridForType<string>();
1533 var logRecords = new Dictionary<object, dynamic>();
1539 string logLines = logText.TrimEnd(Environment.NewLine.ToCharArray());
1541 if (false == string.IsNullOrEmpty(logLines))
1542 foreach (string logLine in logLines.Split(new[] { Environment.NewLine,
"\n" }, StringSplitOptions.None))
1544 logGrid.AddRow(new List<string> { logLine });
1545 logRecords[$
"GPALKEY{logLineIdx++:D4}"] = logLine;
1548 ((IGPALFileInternal)inputFile).FileData.Add(logText);
1549 ((IGPALFileInternal)inputFile).FileGrid.Add(logGrid);
1550 ((IGPALFileInternal)inputFile).FileDictionary.Add(logRecords);
1551 myConverterSettings.InputDictionary = ((IGPALFileInternal)inputFile).FileDictionary;
1554 case DataFormat.JSON:
1555 string fileString = input.ReadToEnd();
1556 input.BaseStream.Position = 0;
1558 dynamic jsonObject = JsonConvert.DeserializeObject<dynamic>(fileString, new JsonConverter[] { new CustomJsonConverter() });
1560 ((IGPALFileInternal)inputFile).FileData.Add(fileString);
1562 var records = new Dictionary<object, dynamic>();
1563 var gridRows = new List<List<string>>();
1566 dynamic ConvertArrayToSurrogateDict(IEnumerable<dynamic> array, int startIndex = 0)
1568 var surrogateDict = new Dictionary<object, dynamic>();
1569 int idx = startIndex;
1570 foreach (dynamic item in array)
1572 surrogateDict[$
"GPALKEY{idx:D4}"] = item;
1575 return surrogateDict;
1579 dynamic ReplaceArrays(dynamic value)
1581 if (value is IEnumerable<dynamic> enumerable && !(value is string))
1583 var items = Enumerable.ToList<dynamic>(enumerable);
1598 bool looksLikePairsArray = items.Count > 0 && items.All(it =>
1599 it is IDictionary d && d.Count == 2
1600 && d.Contains(
"Key") && d.Contains(
"Value"));
1601 if (looksLikePairsArray)
1603 var pairsDict = new Dictionary<object, dynamic>();
1604 foreach (IDictionary pairObj in items)
1606 dynamic pairKey = ReplaceArrays(pairObj[
"Key"]);
1607 dynamic pairValue = ReplaceArrays(pairObj[
"Value"]);
1608 pairsDict[pairKey] = pairValue;
1614 return ConvertArrayToSurrogateDict(items);
1616 else if (value is IDictionary<string, dynamic> dict)
1619 var newDict = new Dictionary<object, dynamic>();
1620 foreach (var kvp in dict)
1622 newDict[kvp.Key] = ReplaceArrays(kvp.Value);
1633 dynamic processedRoot = ReplaceArrays(jsonObject);
1636 if (processedRoot is IDictionary && ConverterHelper.IsArrayLike((IDictionary)processedRoot))
1638 records = (Dictionary<object, dynamic>)processedRoot;
1643 records[
"root"] = processedRoot;
1647 foreach (KeyValuePair<object, dynamic> pair in records.OrderBy(p => p.Key.ToString()))
1649 if (pair.Value is IDictionary rowDict)
1651 var row = new List<string>();
1652 foreach (dynamic kvp in rowDict)
1654 if (kvp.Key != null)
1656 string key = kvp.Key.ToString();
1657 if (key.StartsWith(
"GPALKEY")) continue;
1658 var processedValue = ProcessDictionaryValue(kvp.Value, null, null, key);
1659 if (!(kvp.Value is IDictionary) && !(kvp.Value is IList<object>))
1661 row.Add(FlattenValueForGrid(processedValue));
1672 ((IGPALFileInternal)inputFile).FileDictionary.Add(records);
1674 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"JSON FileDictionary count: [{records.Count}]", null, GPALObjectType.Other);
1676 var gridRow = GPAL.Grid.ToGPALObject();
1677 foreach (var row in gridRows)
1679 gridRow.AddRow(row);
1682 myConverterSettings.InputDictionary = ((IGPALFileInternal)inputFile).FileDictionary;
1683 ((IGPALFileInternal)inputFile).FileGrid.Add(gridRow);
1686 case DataFormat.XLSX:
1687 IGPALGrid<string> grid = ExcelHelper.TokenizeExcelFile(filename2);
1688 List<string> colNames = new List<string>();
1689 List<Dictionary<object, dynamic>> returnList = new List<Dictionary<object, dynamic>>();
1691 ((IGPALFileInternal)inputFile).FileGrid.Add((IGPALGrid<string>)grid.Clone());
1694 using (var workbook = new XLWorkbook(filename))
1696 List<int> rowsPerSheet = new List<int>();
1697 List<string> sheetName = new List<string>();
1699 foreach (IXLWorksheet worksheet in workbook.Worksheets)
1701 int lastRowNumber = worksheet.LastRowUsed()?.RowNumber() ?? 0;
1703 int totalRows = Math.Max(lastRowNumber, worksheet.RangeUsed()?.LastRow().RowNumber() ?? 0);
1705 totalRows = worksheet.Rows().LastOrDefault()?.RowNumber() ?? 0;
1707 rowsPerSheet.Add(totalRows);
1708 sheetName.Add(worksheet.Name);
1711 ((IGPALFileInternal)inputFile).FileSettings.ExcelRowsPerSheet.AddRow(rowsPerSheet);
1712 ((IGPALFileInternal)inputFile).FileSettings.ExcelSheetNames.AddRow(sheetName);
1714 int columnHeaders = 0;
1715 foreach (int rowCnt in rowsPerSheet)
1717 if (myConverterSettings.FirstLineIsColumnHeaders)
1719 colNames = grid[columnHeaders];
1724 foreach (string columnName in grid[columnHeaders].Select(v => (string)v))
1726 colNames.Add($
"Column{colIdx++}");
1730 myConverterSettings.ColumnNames.AddRow(colNames);
1732 if (0 == ((IGPALFileInternal)inputFile).FileSettings.ColumnList.Count())
1733 ((IGPALFileInternal)inputFile).FileSettings.ColumnList.AddRow(colNames);
1737 foreach (Dictionary<object, dynamic> row in ConverterHelper.ConvertGridToDictionary(colNames, grid, columnHeaders, rowCnt))
1739 if (true == myConverterSettings.FirstLineIsColumnHeaders && 0 == rowIdx++)
1742 returnList.Add(row);
1745 columnHeaders += rowCnt;
1751 foreach (int rowCnt in rowsPerSheet)
1753 if (myConverterSettings.FirstLineIsColumnHeaders)
1757 columnHeaders = rowCnt - iteration++;
1761 ((IGPALFileInternal)inputFile).FileDictionary = returnList;
1762 myConverterSettings.InputDictionary = ((IGPALFileInternal)inputFile).FileDictionary;
1765 case DataFormat.XML:
1766 xmlDoc.PreserveWhitespace = true;
1768 input.BaseStream.Position = 0;
1769 var xmlDictionary = ConverterHelper.ConvertXMLToDictionary(xmlDoc);
1770 ((IGPALFileInternal)inputFile).FileDictionary.Add(xmlDictionary);
1771 myConverterSettings.InputDictionary = new List<Dictionary<object, dynamic>> { xmlDictionary };
1772 ((IGPALFileInternal)inputFile).FileData.Add(ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(myConverterSettings, out outGrid));
1773 ((IGPALFileInternal)inputFile).FileGrid.Add(outGrid);
1776 case DataFormat.YAML:
1777 var deserializer = new DeserializerBuilder()
1778 .WithNamingConvention(CamelCaseNamingConvention.Instance)
1781 string yamlText = input.ReadToEnd();
1782 input.BaseStream.Position = 0;
1783 dynamic yamlObject = deserializer.Deserialize<object>(yamlText);
1784 ((IGPALFileInternal)inputFile).FileData.Add(yamlText);
1786 ((IGPALFileInternal)inputFile).FileDictionary.Clear();
1787 ((IGPALFileInternal)inputFile).FileGrid.Clear();
1789 var yamlGridRows = new List<List<string>>();
1790 var processedYamlDict = new Dictionary<dynamic, dynamic>();
1792 if (yamlObject is IDictionary yamlMapping)
1794 foreach (var key in yamlMapping.Keys)
1796 dynamic processedKey = (key is IDictionary || key is IList)
1797 ? ProcessDictionaryValue(key, yamlGridRows)
1798 : (key?.ToString() ??
"null_key");
1799 processedYamlDict[processedKey] = ProcessDictionaryValue(yamlMapping[key], yamlGridRows);
1802 else if (yamlObject is IList yamlSequence)
1807 foreach (var item in yamlSequence)
1809 processedYamlDict[$
"GPALKEY{rootIdx++:D4}"] = ProcessDictionaryValue(item, yamlGridRows);
1815 processedYamlDict[
"GPALKEY0000"] = ProcessDictionaryValue(yamlObject, yamlGridRows);
1818 var yamlGridRow = GPAL.Grid.ToGPALObject();
1819 foreach (var row in yamlGridRows)
1821 yamlGridRow.AddRow(row);
1824 ((IGPALFileInternal)inputFile).FileDictionary.Add(processedYamlDict);
1825 myConverterSettings.InputDictionary = ((IGPALFileInternal)inputFile).FileDictionary;
1826 ((IGPALFileInternal)inputFile).FileGrid.Add(yamlGridRow);
1829 case DataFormat.HTML:
1830 ((IGPALFileInternal)inputFile).FileDictionary = ConverterHelper.ConvertHtmlToDictionary(filename2);
1831 myConverterSettings.InputDictionary = ((IGPALFileInternal)inputFile).FileDictionary;
1832 dynamic inData = ConverterHelper.ConvertInputDictionaryToDelimitedAndGrid(myConverterSettings, out outGrid);
1833 ((IGPALFileInternal)inputFile).FileData.Add(inData);
1834 ((IGPALFileInternal)inputFile).FileGrid.Add(outGrid);
1838 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{inDataFormat}] is not a format GPAL reads, so nothing was tokenized from [{filename}]", inputFile, GPALObjectType.GPALFile);
1839 formatHandled = false;
1846 retval = retval || formatHandled;
1850 catch (Exception ex)
1852 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Problem tokenizing file [{filename}]. Continuing.", inputFile, GPALObjectType.GPALFile, ex);
1857 if (0 == ((IGPALFileInternal)inputFile).FileSettings.FileGrid.Count)
1858 FileHelper.GetGrid(((IGPALFileInternal)inputFile).FileSettings, out tokens);
1860 tokens = ((IGPALFileInternal)inputFile).FileSettings.FileGrid;
1862 ((IGPALFileInternal)inputFile).TokenList = tokens;
1863 ((IGPALFileInternal)inputFile).AlreadyTokenized = retval;
1867 [DllImport(
"kernel32.dll", CharSet = CharSet.Unicode, SetLastError =
true)]
1868 private static extern int GetShortPathName(
string lpszLongPath, StringBuilder lpszShortPath,
int cchBuffer);
1879 var sb =
new StringBuilder(1024);
1880 int result = GetShortPathName(longPath, sb, sb.Capacity);
1881 return result > 0 ? sb.ToString() : longPath;
1890 internal static bool IsWebAddress(
string fileName)
1892 return true == Uri.TryCreate(fileName, UriKind.Absolute, out Uri uri)
1893 && (Uri.UriSchemeHttp == uri.Scheme || Uri.UriSchemeHttps == uri.Scheme);
1908 internal static bool FetchOrDownloadInTab(
string url,
string localPath,
IBrowser browser)
1910 UnitOfWork safeUOW = ((Browser.Browser)browser).CurrentUOW;
1912 bool gotIt = FetchToFile(url, localPath, browser);
1917 gotIt = SaveSourceInTab(url, localPath, browser);
1921 gotIt = DownloadInTab(url, localPath, browser);
1923 ((Browser.Browser)browser).CurrentUOW = safeUOW;
1942 internal static bool SaveSourceInTab(
string url,
string localPath, IBrowser browser)
1945 Browser.Browser gpalBrowser = (Browser.Browser)browser;
1946 int tabsBefore = gpalBrowser.tabsWeOpened;
1947 string wasOn = browser.CurrentUrl;
1949 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Saving [{url}] as a page to [{localPath}].", browser, GPALObjectType.Browser);
1951 browser.NewTab(url).GetPageSource(out
string pageSource);
1955 bool tabIsOurs = tabsBefore < gpalBrowser.tabsWeOpened;
1957 if (
false == tabIsOurs)
1958 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{url}] is not a page the browser renders, so there is no source to save.", browser, GPALObjectType.Browser);
1959 else if (
true ==
string.IsNullOrEmpty(pageSource))
1960 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"[{url}] rendered nothing to save as [{localPath}].", browser, GPALObjectType.Browser);
1963 File.WriteAllText(localPath, pageSource);
1967 if (
true == tabIsOurs)
1970 gpalBrowser.CurrentUrl = wasOn;
1985 internal static int AllowanceFor(
string downloadDirectory, Browser.BrowserSettings browserSettings)
1987 int retVal =
true == IsFinalizing(downloadDirectory, watchArmed)
1988 ? browserSettings.DownloadTimeoutInSec * FinalizeBudgetMultiplier
1989 : browserSettings.DownloadTimeoutInSec;
2005 internal static void DiscardAbandonedDownload(Browser.BrowserSettings browserSettings)
2007 foreach (
string scratch
in watchedScratchFiles)
2010 if (
true == File.Exists(scratch))
2012 File.Delete(scratch);
2013 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);
2016 catch (Exception ex)
2018 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Could not remove [{scratch}]", browserSettings.Browser, GPALObjectType.Browser, ex);
2021 watchedScratchFiles.Clear();
2024 internal const int FinalizeBudgetMultiplier = 6;
2037 internal static bool IsFinalizing(
string downloadDirectory, DateTime since)
2039 bool retVal =
false;
2043 if (
true == Directory.Exists(downloadDirectory))
2049 retVal = Directory.EnumerateFiles(downloadDirectory)
2050 .Any(file =>
true == IsTemporaryDownload(file)
2051 && (File.GetCreationTime(file) >= since || File.GetLastWriteTime(file) >= since));
2053 catch (Exception ex)
2055 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Could not look for a download being finished in [{downloadDirectory}]",
null, GPALObjectType.None, ex);
2061 internal static long DownloadProgress(
string downloadDirectory,
string localPath)
2067 if (
true == File.Exists(localPath))
2068 retVal =
new FileInfo(localPath).Length;
2070 if (
true == Directory.Exists(downloadDirectory))
2071 foreach (
string scratch
in Directory.EnumerateFiles(downloadDirectory).Where(f =>
true == IsTemporaryDownload(f)))
2072 retVal = (0 > retVal ? 0 : retVal) +
new FileInfo(scratch).Length;
2074 catch (Exception ex)
2076 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Could not measure the download in [{downloadDirectory}]",
null, GPALObjectType.None, ex);
2093 internal static bool DownloadInTab(
string url,
string localPath, IBrowser browser)
2095 Browser.BrowserSettings browserSettings = ((Browser.Browser)browser).BrowserSettings;
2097 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Downloading [{url}] with the browser to [{localPath}].", browser, GPALObjectType.Browser);
2103 string landsAt = Path.Combine(downloadDirectory, Path.GetFileName(localPath));
2107 if (
true == browserSettings.UseOttoMagic)
2108 browserSettings.MagicHelper.WithDownloadFile(Path.GetFileName(localPath));
2109 else if (
true == browserSettings.UsePuppeteer)
2110 browserSettings.PuppeteerClient.DownloadTo(downloadDirectory).Execute();
2112 BrowserHelper.SetHeadlessDownload(browserSettings, landsAt);
2116 browserSettings.FileDownloaded =
null;
2117 SetupDownloadWatcher(landsAt, browserSettings);
2119 browser.NewTab(url);
2121 Stopwatch waitingToStart = Stopwatch.StartNew();
2126 long lastSeen =
long.MinValue;
2128 while (
null == browserSettings.FileDownloaded ||
false == browserSettings.FileDownloaded)
2130 long progress = DownloadProgress(downloadDirectory, landsAt);
2132 if (progress != lastSeen)
2134 lastSeen = progress;
2135 waitingToStart.Restart();
2137 else if (waitingToStart.Elapsed.TotalSeconds > AllowanceFor(downloadDirectory, browserSettings))
2146 bool downloaded =
true == browserSettings.FileDownloaded;
2150 if (
true == downloaded &&
false == landsAt.Equals(localPath, StringComparison.OrdinalIgnoreCase))
2152 if (
true == File.Exists(localPath))
2153 File.Delete(localPath);
2155 File.Move(landsAt, localPath);
2156 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{landsAt}] moved to [{localPath}]", browser, GPALObjectType.Browser);
2159 if (
false == downloaded)
2160 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"[{url}] did not download to [{downloadDirectory}] within [{browserSettings.DownloadTimeoutInSec}] seconds.", browser, GPALObjectType.Browser);
2187 private static string CrossOriginNote(
string url, IBrowser browser)
2190 string pageUrl = ((Browser.Browser)browser).BrowserSettings.CurrentURL;
2192 if (
true == Uri.TryCreate(url, UriKind.Absolute, out Uri wanted)
2193 &&
true == Uri.TryCreate(pageUrl, UriKind.Absolute, out Uri onNow)
2194 &&
false ==
string.Equals(wanted.GetLeftPart(UriPartial.Authority), onNow.GetLeftPart(UriPartial.Authority), StringComparison.OrdinalIgnoreCase))
2195 retVal = $
" The page is on [{onNow.GetLeftPart(UriPartial.Authority)}] and the file is on [{wanted.GetLeftPart(UriPartial.Authority)}], so the fetch was cross origin and only works when that server allows it./* Navigating to the file's own site first, or letting the tab download handle it, is what gets past that.*/";
2200 internal static bool FetchToFile(
string url,
string localPath, IBrowser browser)
2202 bool downloaded =
false;
2203 string[] headers =
true == File.Exists(localPath)
2204 ?
new string[] {
"If-Modified-Since", File.GetLastWriteTimeUtc(localPath).ToString(
"R") }
2207 byte[] bytes = BrowserHelper.FetchBytes((Browser.Browser)browser, url, headers, out
string lastModified);
2209 if (304 == browser.ServerResponseCode)
2211 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{url}] has not changed, using [{localPath}].", browser, GPALObjectType.Browser);
2214 else if (
null == bytes)
2215 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{url}] returned no content to save as [{localPath}], HTTP [{browser.ServerResponseCode}].{CrossOriginNote(url, browser)}", browser, GPALObjectType.Browser);
2216 else if (200 > browser.ServerResponseCode || 300 <= browser.ServerResponseCode)
2219 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{url}] returned HTTP [{browser.ServerResponseCode}], nothing saved to [{localPath}].", browser, GPALObjectType.Browser);
2220 else if (0 == bytes.Length)
2221 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{url}] returned an empty file, nothing saved to [{localPath}].", browser, GPALObjectType.Browser);
2224 File.WriteAllBytes(localPath, bytes);
2228 if (
true == DateTime.TryParse(lastModified, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AdjustToUniversal, out DateTime modified))
2229 File.SetLastWriteTimeUtc(localPath, modified);
2231 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Downloaded [{url}] with the browser to [{localPath}].", browser, GPALObjectType.Browser);
2248 if (!directoryPath.EndsWith(
"\\"))
2251 directoryPath +=
"\\";
2253 return directoryPath;