runtime: FTP Downloading Randomly Missing Bytes in .NET 6.0

Description

I’m putting some FTP functionality into a MAUI app (Android/Windows Machine) that I’m working on to backup and restore the app’s SQLite database. The upload works just fine. But the download only works maybe 1 time out of 5. I’m finding that a random number of bytes are ‘missing’ from the download. For example, if the database file is 2,179,072 bytes large, sometimes the downloaded file will only have 2,179,070 bytes, or 2,178,168 bytes, or 2,179,007 bytes, etc. It’s completely random. I was initially thinking it was just an issue with the way the database get’s written once it’s downloaded, but the database becomes corrupted if it’s missing pieces.

But here’s where it gets interesting: I tried the code in a separate WPF desktop application. The exact same code works perfectly fine in the full .NET Framework 4.7.2. It’s just an issue with .NET Core 6.0.

Reproduction Steps

Add this code to a test app and call TestFtpDownload(). You’ll need credentials to an FTP server as I replaced my credentials with placeholders.

  public FtpsUtilities.FtpsCredentials MurrayFtpCredentials
  { get; } = new FtpsUtilities.FtpsCredentials($"{MurrayFtpAccount}@murraycontrolpro.com", MurrayFtpPassword);
  private static string ApplicationDatabasePath
  { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), DatabaseFileName);
  private static string TempApplicationDatabasePath
  { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), $"_{DatabaseFileName}");

  private const string MurrayFtpAccount = "ftpUsername";

  private const string MurrayFtpPassword = "ftpPassword";
  private const string FtpBackupFolder = "TestFolder01";

  private const string MurrayFtpHost = @"ftp://testftp.com";

  private const string MurrayFtpDatabaseFilesFolder = "DatabaseFiles";
  private const string DatabaseFileName = "database.db3";

  private void TestFtpDownload()
  {
   FileSpaceUtilities.DeleteFile(TempApplicationDatabasePath);
   string serverPath = FtpPathCombine(MurrayFtpHost, MurrayFtpDatabaseFilesFolder);
   serverPath = FtpPathCombine(serverPath, FtpBackupFolder);
   serverPath = FtpPathCombine(serverPath, DatabaseFileName);
   string message = DownloadFile2(serverPath, TempApplicationDatabasePath, false, MurrayFtpCredentials);
  }

  public static string FtpPathCombine(string serverPath, string file) => $"{serverPath}/{file}";

  public static string DownloadFile2(
   string serverPath,
   string localPath,
   bool deleteAfterDownload,
   FtpsCredentials credentials)
  {
   try
   {
    long fileSize = GetFileSize(serverPath, credentials);
    long bytesRead = 0;
    FtpWebRequest request = CreateRequest(serverPath, WebRequestMethods.Ftp.DownloadFile, credentials);
    using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
    using (Stream responseStream = response.GetResponseStream())
    using (FileStream fileStream = File.Create(localPath))
    {
     byte[] buffer = new byte[2048];
     while (true)
     {
      int read = responseStream.Read(buffer, 0, buffer.Length);
      if (read <= 0)
       break;
      bytesRead += read;
      fileStream.Write(buffer, 0, read);
     }
    }

    FileInfo fileInfo = new FileInfo(localPath);
    if (fileSize != bytesRead)
     return $"File size mismatch. FTP: {fileSize:n0}; local: {bytesRead:n0}";

    if (deleteAfterDownload)
     DeleteFile(serverPath, credentials);
   }
   catch (Exception e)
   {
    string message = e.GetInnerMessage();
    return message;
   }
   return null;
  }

  private static FtpWebRequest CreateRequest(string serverPath, string method, FtpsCredentials credentials)
  {
   FtpWebRequest request = (FtpWebRequest)WebRequest.Create(new Uri(serverPath));
   request.Credentials = new NetworkCredential(credentials.UserName, credentials.Password);
   request.UseBinary = true;
   request.UsePassive = true;
   request.Proxy = null;
   request.KeepAlive = true;
   request.Method = method;
   request.Timeout = RequestTimeOut;
   return request;
  }

  private static long GetFileSize(string serverPath, FtpsCredentials credentials)
  {
   long fileSize = -1;
   FtpWebRequest request = CreateRequest(serverPath, WebRequestMethods.Ftp.GetFileSize, credentials);
   using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
   {
    fileSize = response.ContentLength;
    response.Close();
   }
   return fileSize;
  }

  public class FtpsCredentials
  {
   public FtpsCredentials(string userName, string password)
   {
    UserName = userName;
    Password = password;
   }

   public string UserName;
   public string Password;
  }

Expected behavior

The file downloads with all bytes. In other words, the size of the locally created file will match the size of the file on the FTP server exactly.

Actual behavior

When executed in an solution using .NET Framework 4.7.2, the file downloads correctly. When executed in a solution using .NET Core 6.0, bytes are missing.

Regression?

No response

Known Workarounds

Keep trying it until it works.

Configuration

No response

Other information

MS devs can reach out to me and I’ll provide FTP credentials to our FTP server if needed.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 17 (11 by maintainers)

Most upvoted comments

It may be worth of checking if this is Android specific issue. e.g. does the code run fine on plain Linux or Windows?

The fileInfo.Length and the bytesRead have always matched. So, I just check one.

I’m in the process of installing a new API 32 emulator to see if the problem persists…

I can’t reproduce it locally (my Android device running API 31 and an emulator running API 30). It always succeeds. And I can’t think of a reason why this should skip a few bytes 🤔