Skip to content

Commit

Permalink
improved newest file selection in ftp-import (closes #1289)
Browse files Browse the repository at this point in the history
  • Loading branch information
cadon committed Oct 1, 2022
1 parent 59d072d commit ee6fc71
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 47 deletions.
98 changes: 76 additions & 22 deletions ARKBreedingStats/Form1.importSave.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using ARKBreedingStats.uiControls;
using ARKBreedingStats.utils;

namespace ARKBreedingStats
Expand Down Expand Up @@ -50,18 +49,32 @@ private async Task<string> 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}");
Expand Down Expand Up @@ -131,7 +144,7 @@ private async Task<string> RunSavegameImport(ATImportFileLocation atImportFileLo
return null; // no error
}

private async Task<string> 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);
Expand All @@ -150,7 +163,7 @@ private async Task<string> 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;
Expand All @@ -164,9 +177,13 @@ private async Task<string> 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);
Expand All @@ -181,14 +198,15 @@ private async Task<string> 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;
Expand All @@ -212,7 +230,7 @@ private async Task<string> CopyFtpFileAsync(Uri ftpUri, string serverName, strin
filePath = await DecompressGZippedFileAsync(filePath, cancellationTokenSource.Token);
}

return filePath;
return (filePath, null);
}
catch (FtpAuthenticationException ex)
{
Expand All @@ -224,16 +242,18 @@ private async Task<string> 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
{
Expand Down Expand Up @@ -282,17 +302,51 @@ private async Task<string> DecompressGZippedFileAsync(string filePath, Cancellat
return newFileName;
}

public async Task<FtpListItem> GetLastModifiedFileAsync(FtpClient client, Uri ftpUri, CancellationToken cancellationToken)
public async Task<FtpListItem> 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;
}

/// <summary>
Expand Down
14 changes: 14 additions & 0 deletions ARKBreedingStats/settings/ATImportFileLocationDialog.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions ARKBreedingStats/settings/ATImportFileLocationDialog.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;
using ARKBreedingStats.utils;
Expand Down Expand Up @@ -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");
}
}
}
15 changes: 8 additions & 7 deletions ARKBreedingStats/settings/FtpProgress.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 7 additions & 17 deletions ARKBreedingStats/settings/FtpProgress.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using ARKBreedingStats.miscClasses;
using FluentFTP;
using FluentFTP;
using System;
using System.Diagnostics;
using System.Threading;
Expand All @@ -17,35 +16,26 @@ 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;
}

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();
}
}
2 changes: 1 addition & 1 deletion ARKBreedingStats/settings/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ private void btAddSavegameFileLocation_Click(object sender, EventArgs e)
/// </summary>
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);
}
Expand Down

0 comments on commit ee6fc71

Please sign in to comment.