diff --git a/ARKBreedingStats/Form1.importSave.cs b/ARKBreedingStats/Form1.importSave.cs index dc7fe8cf..3370a3e2 100644 --- a/ARKBreedingStats/Form1.importSave.cs +++ b/ARKBreedingStats/Form1.importSave.cs @@ -12,7 +12,6 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -using ARKBreedingStats.uiControls; using ARKBreedingStats.utils; namespace ARKBreedingStats @@ -50,18 +49,32 @@ private async Task RunSavegameImport(ATImportFileLocation atImportFileLo workingCopyFolderPath = Path.GetTempPath(); } + var fileLocation = atImportFileLocation.FileLocation; + string uriFileRegex = null; - if (Uri.TryCreate(atImportFileLocation.FileLocation, UriKind.Absolute, out var uri) + var indexLastSlash = fileLocation.LastIndexOf('/'); + if (indexLastSlash > 0) + { + var lastUriSegment = fileLocation.Split('/').Last(); + if (lastUriSegment.Contains("*") || lastUriSegment.Contains("(?<")) + { + fileLocation = fileLocation.Substring(0, indexLastSlash); + uriFileRegex = lastUriSegment; + } + } + + if (Uri.TryCreate(fileLocation, UriKind.Absolute, out var uri) && uri.Scheme != "file") { switch (uri.Scheme) { case "ftp": - workingCopyFilePath = await CopyFtpFileAsync(uri, atImportFileLocation.ConvenientName, + string errorMessage; + (workingCopyFilePath, errorMessage) = await CopyFtpFileAsync(uri, uriFileRegex, atImportFileLocation.ConvenientName, workingCopyFolderPath); - if (workingCopyFilePath == null) + if (errorMessage != null) // the user didn't enter credentials - return "no credentials"; + return errorMessage; break; default: throw new Exception($"Unsupported uri scheme: {uri.Scheme}"); @@ -131,7 +144,7 @@ await ImportSavegame.ImportCollectionFromSavegame(_creatureCollection, workingCo return null; // no error } - private async Task CopyFtpFileAsync(Uri ftpUri, string serverName, string workingCopyFolder) + private async Task<(string filePath, string errorMessage)> CopyFtpFileAsync(Uri ftpUri, string fileRegex, string serverName, string workingCopyFolder) { var credentialsByServerName = LoadSavedCredentials(); credentialsByServerName.TryGetValue(serverName, out var credentials); @@ -150,7 +163,7 @@ private async Task CopyFtpFileAsync(Uri ftpUri, string serverName, strin { if (dialog.ShowDialog(this) == DialogResult.Cancel) { - return null; + return (null, "no credentials given, aborted by user"); } credentials = dialog.Credentials; @@ -164,9 +177,13 @@ private async Task CopyFtpFileAsync(Uri ftpUri, string serverName, strin } } var client = new FtpClient(ftpUri.Host, ftpUri.Port, credentials.Username, credentials.Password); + string ftpPath = null; try { + if (progressDialog.IsDisposed) + return (null, "aborted by user"); + progressDialog.StatusText = $"Authenticating on server {serverName}"; if (!progressDialog.Visible) progressDialog.Show(this); @@ -181,14 +198,15 @@ private async Task CopyFtpFileAsync(Uri ftpUri, string serverName, strin progressDialog.StatusText = "Finding most recent file"; await Task.Yield(); - var ftpPath = ftpUri.AbsolutePath; - var lastSegment = ftpUri.Segments.Last(); - if (lastSegment.Contains("*")) + ftpPath = ftpUri.AbsolutePath; + + if (fileRegex != null) { - var mostRecentlyModifiedMatch = await GetLastModifiedFileAsync(client, ftpUri, cancellationTokenSource.Token); + var mostRecentlyModifiedMatch = + await GetLastModifiedFileAsync(client, ftpUri, fileRegex, cancellationTokenSource.Token); if (mostRecentlyModifiedMatch == null) { - throw new Exception($"No file found matching pattern '{lastSegment}'"); + throw new Exception($"No file found matching pattern '{fileRegex}'"); } ftpPath = mostRecentlyModifiedMatch.FullName; @@ -212,7 +230,7 @@ private async Task CopyFtpFileAsync(Uri ftpUri, string serverName, strin filePath = await DecompressGZippedFileAsync(filePath, cancellationTokenSource.Token); } - return filePath; + return (filePath, null); } catch (FtpAuthenticationException ex) { @@ -224,16 +242,18 @@ private async Task CopyFtpFileAsync(Uri ftpUri, string serverName, strin catch (OperationCanceledException) { client.Dispose(); - return null; + return (null, "aborted by user"); } catch (Exception ex) { + var errorMessage = $"Unexpected error while downloading file\n{ftpPath}:\n{ex.Message}{(string.IsNullOrEmpty(ex.InnerException?.Message) ? null : "\n\nInner Exception:\n" + ex.InnerException?.Message)}"; if (progressDialog.IsDisposed) { client.Dispose(); - return null; + return (null, errorMessage); } - progressDialog.StatusText = $"Unexpected error: {ex.Message}"; + progressDialog.StatusText = errorMessage + "\n\nTrying again in some seconds."; + await Task.Delay(3000); } finally { @@ -282,17 +302,51 @@ private async Task DecompressGZippedFileAsync(string filePath, Cancellat return newFileName; } - public async Task GetLastModifiedFileAsync(FtpClient client, Uri ftpUri, CancellationToken cancellationToken) + public async Task GetLastModifiedFileAsync(FtpClient client, Uri ftpUri, string fileRegex, CancellationToken cancellationToken) { var folderUri = new Uri(ftpUri, "."); var listItems = await client.GetListingAsync(folderUri.AbsolutePath, cancellationToken); - // Turn the wildcard into a regex pattern "super*.foo" -> "^super.*?\.foo$" - var nameRegex = new Regex("^" + Regex.Escape(ftpUri.Segments.Last()).Replace(@"\*", ".*?") + "$"); + Regex fileNameRegex; + if (!fileRegex.Contains("(?<")) + { + // assume only simple wildcard + // Turn the wildcard into a regex pattern "super*.foo" -> "^super.*?\.foo$" + fileNameRegex = new Regex("^" + Regex.Escape(fileRegex).Replace(@"\*", ".*?") + "$"); + + return listItems + .OrderByDescending(x => x.Modified) + .FirstOrDefault(x => fileNameRegex.IsMatch(x.Name)); + } + + fileNameRegex = new Regex(fileRegex); + + // order by named groups descending + var listWithMatches = listItems.Select(f => (ftpFile: f, match: fileNameRegex.Match(f.Name))).Where(f => f.Item2.Success).ToArray(); + + switch (listWithMatches.Length) + { + case 0: return null; + case 1: return listWithMatches[0].ftpFile; + } + + var regexGroupNames = fileNameRegex.GetGroupNames().Where(n => n != "0").OrderBy(n => n).ToArray(); + if (regexGroupNames.Length == 0) + return listWithMatches.First().ftpFile; + + var orderedListWithMatches = + listWithMatches.OrderByDescending(f => f.match.Groups[regexGroupNames[0]].Value); + + for (int g = 1; g < regexGroupNames.Length; g++) + { + var groupName = regexGroupNames[g]; // closure + orderedListWithMatches = + orderedListWithMatches.ThenByDescending(f => f.match.Groups[groupName].Value); + } + + var orderedList = orderedListWithMatches.ToArray(); - return listItems - .OrderByDescending(x => x.Modified) - .FirstOrDefault(x => nameRegex.IsMatch(x.Name)); + return orderedList.First().ftpFile; } /// diff --git a/ARKBreedingStats/settings/ATImportFileLocationDialog.Designer.cs b/ARKBreedingStats/settings/ATImportFileLocationDialog.Designer.cs index 3b6f4dd2..8fed40d9 100644 --- a/ARKBreedingStats/settings/ATImportFileLocationDialog.Designer.cs +++ b/ARKBreedingStats/settings/ATImportFileLocationDialog.Designer.cs @@ -33,6 +33,7 @@ private void InitializeComponent() { this.button_FileSelect = new System.Windows.Forms.Button(); this.button_Cancel = new System.Windows.Forms.Button(); this.button_Ok = new System.Windows.Forms.Button(); + this.LlFtpHelp = new System.Windows.Forms.LinkLabel(); this.SuspendLayout(); // // label_ConvenientName @@ -117,6 +118,17 @@ private void InitializeComponent() { this.button_Ok.Text = "OK"; this.button_Ok.UseVisualStyleBackColor = true; // + // LlFtpHelp + // + this.LlFtpHelp.AutoSize = true; + this.LlFtpHelp.Location = new System.Drawing.Point(12, 128); + this.LlFtpHelp.Name = "LlFtpHelp"; + this.LlFtpHelp.Size = new System.Drawing.Size(164, 13); + this.LlFtpHelp.TabIndex = 9; + this.LlFtpHelp.TabStop = true; + this.LlFtpHelp.Text = "Manual for configuring ftp access"; + this.LlFtpHelp.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.LlFtpHelp_LinkClicked); + // // ATImportFileLocationDialog // this.AcceptButton = this.button_Ok; @@ -124,6 +136,7 @@ private void InitializeComponent() { this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.button_Cancel; this.ClientSize = new System.Drawing.Size(448, 158); + this.Controls.Add(this.LlFtpHelp); this.Controls.Add(this.button_Ok); this.Controls.Add(this.button_Cancel); this.Controls.Add(this.button_FileSelect); @@ -158,5 +171,6 @@ private void InitializeComponent() { private System.Windows.Forms.Button button_FileSelect; private System.Windows.Forms.Button button_Cancel; private System.Windows.Forms.Button button_Ok; + private System.Windows.Forms.LinkLabel LlFtpHelp; } } \ No newline at end of file diff --git a/ARKBreedingStats/settings/ATImportFileLocationDialog.cs b/ARKBreedingStats/settings/ATImportFileLocationDialog.cs index f85bb547..5b319e1a 100644 --- a/ARKBreedingStats/settings/ATImportFileLocationDialog.cs +++ b/ARKBreedingStats/settings/ATImportFileLocationDialog.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.Windows.Forms; using ARKBreedingStats.utils; @@ -58,5 +59,10 @@ private void button_FileSelect_Click(object sender, EventArgs e) } } } + + private void LlFtpHelp_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + RepositoryInfo.OpenWikiPage("Ftp-access"); + } } } diff --git a/ARKBreedingStats/settings/FtpProgress.Designer.cs b/ARKBreedingStats/settings/FtpProgress.Designer.cs index 477acfd1..64e10b73 100644 --- a/ARKBreedingStats/settings/FtpProgress.Designer.cs +++ b/ARKBreedingStats/settings/FtpProgress.Designer.cs @@ -36,7 +36,7 @@ private void InitializeComponent() // this.button_Cancel.Anchor = System.Windows.Forms.AnchorStyles.Bottom; this.button_Cancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.button_Cancel.Location = new System.Drawing.Point(155, 71); + this.button_Cancel.Location = new System.Drawing.Point(155, 109); this.button_Cancel.Name = "button_Cancel"; this.button_Cancel.Size = new System.Drawing.Size(75, 23); this.button_Cancel.TabIndex = 9; @@ -46,11 +46,12 @@ private void InitializeComponent() // // StatusLabel // - this.StatusLabel.Anchor = System.Windows.Forms.AnchorStyles.Top; + this.StatusLabel.Dock = System.Windows.Forms.DockStyle.Fill; this.StatusLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.StatusLabel.Location = new System.Drawing.Point(12, 9); + this.StatusLabel.Location = new System.Drawing.Point(0, 0); this.StatusLabel.Name = "StatusLabel"; - this.StatusLabel.Size = new System.Drawing.Size(360, 59); + this.StatusLabel.Padding = new System.Windows.Forms.Padding(0, 0, 0, 30); + this.StatusLabel.Size = new System.Drawing.Size(384, 144); this.StatusLabel.TabIndex = 10; this.StatusLabel.Text = "Status Text"; this.StatusLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; @@ -60,10 +61,10 @@ private void InitializeComponent() this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.button_Cancel; - this.ClientSize = new System.Drawing.Size(384, 106); - this.Controls.Add(this.StatusLabel); + this.ClientSize = new System.Drawing.Size(384, 144); this.Controls.Add(this.button_Cancel); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.Controls.Add(this.StatusLabel); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; this.MaximizeBox = false; this.MinimizeBox = false; this.Name = "FtpProgressForm"; diff --git a/ARKBreedingStats/settings/FtpProgress.cs b/ARKBreedingStats/settings/FtpProgress.cs index 99716acc..75ebbf22 100644 --- a/ARKBreedingStats/settings/FtpProgress.cs +++ b/ARKBreedingStats/settings/FtpProgress.cs @@ -1,5 +1,4 @@ -using ARKBreedingStats.miscClasses; -using FluentFTP; +using FluentFTP; using System; using System.Diagnostics; using System.Threading; @@ -17,22 +16,16 @@ public FtpProgressForm(CancellationTokenSource cancellationTokenSource) public string StatusText { - get - { - return StatusLabel.Text; - } - set - { - StatusLabel.Text = value; - } + get => StatusLabel.Text; + set => StatusLabel.Text = value; } public string FileName { get; set; } - private Stopwatch stopwatch = new Stopwatch(); + private readonly Stopwatch _stopwatch = new Stopwatch(); public void Report(FtpProgress value) { - if (value.Progress < 100 && stopwatch.IsRunning && stopwatch.ElapsedMilliseconds < 250) + if (value.Progress < 100 && _stopwatch.IsRunning && _stopwatch.ElapsedMilliseconds < 250) { // only update the progress every 250ms unless setting it to 100% return; @@ -40,12 +33,9 @@ public void Report(FtpProgress value) var statusText = $"Downloading {FileName}\r\n{value.Progress:F0}% complete\r\n{value.TransferSpeedToString()}"; StatusLabel.Invoke(new Action(() => StatusLabel.Text = statusText)); - stopwatch.Restart(); + _stopwatch.Restart(); } - private void button_Cancel_Click(object sender, EventArgs e) - { - this.Close(); - } + private void button_Cancel_Click(object sender, EventArgs e) => Close(); } } diff --git a/ARKBreedingStats/settings/Settings.cs b/ARKBreedingStats/settings/Settings.cs index 435da2f3..02c52e54 100644 --- a/ARKBreedingStats/settings/Settings.cs +++ b/ARKBreedingStats/settings/Settings.cs @@ -643,7 +643,7 @@ private void btAddSavegameFileLocation_Click(object sender, EventArgs e) /// private void CheckSaveImportPath(string filePath) { - if (!filePath.EndsWith(".ark") && !filePath.Contains("*")) + if (!filePath.EndsWith(".ark") && !filePath.EndsWith(".gz") && !filePath.Contains("*") && !filePath.Contains("(?<")) { MessageBoxes.ShowMessageBox($"The file location must include the path and the filename of the save file. The set path\n{filePath}\ndoesn't end with \".ark\" and seems to miss the file name.", "Possibly wrong path", MessageBoxIcon.Warning); }