KnowDotNet

Complex File Masking for FTP Component

Mask Any Filename and Extension

by Les Smith

Have you ever needed to mask files being pulled from an FTP site?   Many compontents pull down and delete all files from a folder.  That may not be what you want.  This article shows you how to implement sophisticated file masking in the FTP component to pull only the types and or names you want to retrieve.

I have a requirement to be able to selectively pull files from an FTP Folder.  Some of the clienst with which I am dealing insist on mixing file types and names in the same folder although the files and types go to different applications.  In a simple FTP component, the Download method usually pulls and alternately delets all of the files found in the folder.  If you delete the files pulled down from the FTP folder and then find that the files do not all belong to the application for which you are pulling files, you have to jump through hoops to put the unwanted files back on the FTP folder.  At the least you will have to separate the download and delete logic and put some filemasking in the middle.

This article will demonstrate the code for a complete filmasking methodology.  You can either implement the code inside of your FTP component, assuming you have the code for it.  Optionally, you can call it between the time that you download files and the time that you delete the files via the FTP component.  The point is that as in my case I need to be able to mask both the filename and the extension.  This article will give you the code to do it in both VB.NET and C#.

Let me say up front that the code you see is much more compact and straight forward than it was when I first started.  If you think about the problem, it is not trivial and if you try to write it in one method, you will soon have code that is very hard to understand and debeg and ensure that you have covered all cases.  As you may know if you have read any of my many articles on Refactoring, I am big on it.  

In addition to studying and writing about Refactoring, you probably know that our KnowDotNet team has written NetRefactor which is a comprehensive refactoring tool for both C# and VB.NET.  As a result of these activities, I have developed a refactoring mindset as I code new applications.  I rarely write a method any more that does more than one major thing, unless it is a "driver" method that call many "worker" methods that accomplish what I used to do in one method.  That type of design is readily apparent in the code shown in this article.  

The goal in this code is to allow any combination of starting or ending wildcard characters in both the filename and the extension.  That means that the code should be able to handle a filename and extension mask combination that is represented by the following expression.

    
"*|[*]filenamepart[*].*|[*]extpart[*]"

Figure 1 will show the VB.NET code for the main file masking method.  The helper methods will be shown after it.

Figure 1 - VB.NET File Masking.

   Private Enum wcPos
      none
      wrap
      start
      [end]
  
End Enum

   ''' <summary>
   ''' Filemask can be "*|[*]filenamepart[*].*|[*]extpart[*]"
   ''' </summary>
   ''' <param name="fileName" ></param>
   ''' <param name="fileMask" ></param>
   ''' <returns>Boolean</returns>
   ''' <remarks>
   ''' Handles positional start and/or ending wildcards.
   ''' </remarks>
   Private Function FileMatchesMask(ByVal fileName As String, _
      ByVal fileMask As String) As Boolean
      Const asterisk As String = "*"
      Dim sE As String = String.Empty
      
If fileMask.Equals("*.*") Then Return True

      ' first, get comparable filenames and extensions
      Dim fn As String = Path.GetFileNameWithoutExtension(fileName).ToLower
      
Dim ext As String = Path.GetExtension(fileName).Replace(".", sE).ToLower

      
Dim maskName As String = Path.GetFileNameWithoutExtension(fileMask).ToLower
      
Dim maskExt As String = Path.GetExtension(fileMask).ToLower.Replace(".", sE)
      
Dim maskExtStripped As String = maskExt.ToLower.Replace(asterisk, sE)
      
Dim maskNameStripped As String = maskName.ToLower.Replace(asterisk, sE)
      
Dim fnWCP As wcPos = GetWildCardPosition(maskName)
      
Dim extWCP As wcPos = GetWildCardPosition(maskExt)

      
If maskExt.Equals(asterisk) Then
         ' ext can be anything, but fn may be masked
         If maskName.IndexOf(asterisk) > -1 Then
            ' fn masked, ck position of wildcard
            Select Case fnWCP
              
Case wcPos.wrap : Return IsValueMatch(fn, maskNameStripped, wcPos.wrap)
              
Case wcPos.start : Return IsValueMatch(fn, maskNameStripped, wcPos.start)
              
Case wcPos.end : Return IsValueMatch(fn, maskNameStripped, wcPos.end)
              
Case Else : Return IsValueMatch(fn, maskNameStripped, wcPos.wrap)
            
End Select
         Else
            ' need exact match on filename, any ext is ok
            If maskName.Equals(fn) Then
               Return True
            End If
         End If
      ElseIf maskName.Equals(asterisk) Then
         ' fn can be anything, but ext may be masked
         If maskExt.IndexOf(asterisk) > -1 Then
            ' ext masked, ck position of wildcard
            Select Case extWCP
              
Case wcPos.wrap : Return IsValueMatch(ext, maskExtStripped, wcPos.wrap)
              
Case wcPos.start : Return IsValueMatch(ext, maskExtStripped, wcPos.start)
              
Case wcPos.end : Return IsValueMatch(ext, maskExtStripped, wcPos.end)
              
Case Else : Return IsValueMatch(ext, maskExtStripped, wcPos.wrap)
            
End Select
         Else
            ' need exact match on ext, any filename is ok
            If maskExt.Equals(ext) Then
               Return True
            End If
         End If
      Else
         ' here neither fn or ext = asterisk
         ' so must ck for existence of asterisk in either
         ' b/c of position of * this will get hairy...
         If extWCP <> wcPos.none Then
            ' ext is masked, ck for masking in fn
            If maskName.IndexOf(asterisk) > -1 Then
               ' ext and fn are masked
               Return IsValueMatch(fn, maskNameStripped, fnWCP) And _
                      IsValueMatch(ext, maskExtStripped, extWCP)
            
Else
               ' fn not masked, look for fn exact match and ext masked
               If fn.Equals(maskName) AndAlso _
                  IsValueMatch(ext, maskExtStripped, extWCP)
Then
                  Return True
               End If
            End If
         Else
            ' ext is not masked, requires exact ext and ck for fn masking
            If maskName.IndexOf(asterisk) > -1 Then
               ' fn masked
                If IsValueMatch(fn, maskNameStripped, fnWCP) AndAlso _
                   ext.Equals(maskExtStripped)
Then
                  Return True
               ElseIf fn.Equals(maskName) AndAlso _
                      ext.Equals(maskExtStripped)
Then
                  Return True
               End If
            End If
         End If
      End If
      Return False
   End Function

Figure 2 - Helper Methods.

   ''' <summary>
   ''' Returns position of existing wildcard[s].
   ''' start, wrap, end, none
   ''' </summary>
   ''' <param name = "mask"></param>
   ''' <returns>wcPos</returns>
   Private Function GetWildCardPosition(ByVal mask As String) As wcPos
      
Dim wildStart As String = "^\*\w+"
      Dim wildEnd As String = "^[a-z0-9_ ]*\*"
      Dim wildWrap As String = "^\*[a-z0-9_ ]*\*"
      If mask.Equals("*") Then
         Return wcPos.none
      
ElseIf Regex.IsMatch(mask, wildWrap) Then
         Return wcPos.wrap
      
ElseIf Regex.IsMatch(mask, wildStart) Then
         Return wcPos.start
      
ElseIf Regex.IsMatch(mask, wildEnd) Then
         Return wcPos.end
      
Else
         Return wcPos.none
      
End If
   End Function


   ''' <summary>
   ''' Returns true if mask matches value, based on position specifier.
   ''' </summary>
   ''' <param name = "value"></param>
   ''' <param name = "mask"></param>
   ''' <returns>Boolean</returns>
   Private Function IsValueMatch(ByVal value As String,
      
ByVal mask As String, ByVal position As wcPos) As Boolean
      Select Case position
        
Case wcPos.wrap : If value.IndexOf(mask) > -1 Then Return True
         Case wcPos.end : If value.StartsWith(mask) Then Return True
         Case wcPos.start : If value.EndsWith(mask) Then Return True
      End Select
   End Function

The code shown in Figure 3 will test the Filemasking methods.  To use this code in C#, just place a ";" behind each line and before the comment obviously.  The comment denotes the result that should be printed in the console output window.


Figure 3 - Test Code for Filemasking.

      Console.WriteLine(FileMatchesMask("filename.txt", "*.*").ToString) ' true
      Console.WriteLine(FileMatchesMask(
"filename.txt", "*.txt").ToString) ' true
      Console.WriteLine(FileMatchesMask(
"filename.text", "*.*ex*").ToString) ' true
      Console.WriteLine(FileMatchesMask(
"filename.exe", "*.ex*").ToString) ' true
      Console.WriteLine(FileMatchesMask(
"filename.exe", "*.*xe").ToString) ' true
      Console.WriteLine(FileMatchesMask(
"filename.txt", "filename.*").ToString) ' true
      Console.WriteLine(FileMatchesMask(
"filename.txt", "*ilenam*.*").ToString) ' true
      Console.WriteLine(FileMatchesMask("FILENAME_90.txt", "filename*.*").ToString) ' true
      Console.WriteLine(FileMatchesMask("90_FILENAME.txt", "*filename*.*").ToString) ' true
      Console.WriteLine(FileMatchesMask("90_FILENAME.txt", "filename.*").ToString) ' false
      Console.WriteLine(FileMatchesMask("filename.txt", "*.tx").ToString) ' false
      Console.WriteLine(FileMatchesMask("filename.text", "*.ex*").ToString) 'false
      Console.WriteLine(FileMatchesMask("filename.exe", "*.*xe").ToString) 'true
      Console.WriteLine(FileMatchesMask("filename.exe", "*.xe*").ToString) 'false
      Console.WriteLine(FileMatchesMask("filename.txt", "filenam.*").ToString) 'false
      Console.WriteLine(FileMatchesMask("filename.txt", "*ilnam*.*").ToString) ' true
      Console.WriteLine(FileMatchesMask("FILENAME_90.txt", "filename.*").ToString) ' true


Figure 4 - C# Code for Filemasking.

   private enum wcPos: int
   {
      none,
      wrap,
      start,
      @end
   }
  
/// <summary>
  
/// Filemask can be "*|[*]filenamepart[*].*|[*]extpart[*]"
  
/// </summary>
  
/// <param name="fileName" ></param>
  
/// <param name="fileMask" ></param>
  
/// <returns>Boolean</returns>
  
/// <remarks>
  
/// Handles positional start and/or ending wildcards.
  
/// </remarks>
  
private bool FileMatchesMask(string fileName, string fileMask)
   {
      
const string asterisk = "*";
      
string se = string.Empty;
      
if (fileMask.Equals("*.*"))
        
return true;

      
// first, get comparable filenames and extensions
      
string fn = Path.GetFileNameWithoutExtension(fileName).ToLower();
      
string ext = Path.GetExtension(fileName).Replace(".", se).ToLower();

      
string maskName = Path.GetFileNameWithoutExtension(fileMask).ToLower();
      
string maskExt = Path.GetExtension(fileMask).ToLower().Replace(".", se);
      
string maskExtStripped = maskExt.ToLower().Replace(asterisk, se);
      
string maskNameStripped = maskName.ToLower().Replace(asterisk, se);
      wcPos fnWCP = GetWildCardPosition(maskName);
      wcPos extWCP = GetWildCardPosition(maskExt);

      
if (maskExt.Equals(asterisk))
      {
        
// ext can be anything, but fn may be masked
        
if (maskName.IndexOf(asterisk) > -1)
        {
        
// fn masked, ck position of wildcard
        
switch (fnWCP)
         {
            
case wcPos.wrap:
            
return IsValueMatch(fn, maskNameStripped, wcPos.wrap);
            
case wcPos.start:
            
return IsValueMatch(fn, maskNameStripped, wcPos.start);
            
case wcPos.@end:
            
return IsValueMatch(fn, maskNameStripped, wcPos.@end);
            
default:
            
return IsValueMatch(fn, maskNameStripped, wcPos.wrap);
         }
        }
        
else
        {
        
// need exact match on filename, any ext is ok
        
if (maskName.Equals(fn))
            
return true;
        }
      }
      
else if (maskName.Equals(asterisk))
      {
        
// fn can be anything, but ext may be masked
        
if (maskExt.IndexOf(asterisk) > -1)
        {
        
// ext masked, ck position of wildcard
        
switch (extWCP)
         {
            
case wcPos.wrap:
            
return IsValueMatch(ext, maskExtStripped, wcPos.wrap);
            
case wcPos.start:
            
return IsValueMatch(ext, maskExtStripped, wcPos.start);
            
case wcPos.@end:
            
return IsValueMatch(ext, maskExtStripped, wcPos.@end);
            
default:
            
return IsValueMatch(ext, maskExtStripped, wcPos.wrap);
         }
        }
        
else
        {
        
// need exact match on ext, any filename is ok
        
if (maskExt.Equals(ext))
            
return true;
        }
      }
      
else
      {
        
// here neither fn or ext = asterisk so must ck for
             // existence of asterisk in either

        
// b/c of position of * this will get hairy...
        
if (extWCP != wcPos.none)
        {
        
// ext is masked, ck for masking in fn
        
if (maskName.IndexOf(asterisk) > -1)
         {
            
// ext and fn are masked
            
return IsValueMatch(fn, maskNameStripped, fnWCP) &
                            IsValueMatch(ext, maskExtStripped, extWCP);
         }
        
else
         {
            
// fn not masked, look for fn exact match and
                     // ext masked

            
if (fn.Equals(maskName) &&
                        IsValueMatch(ext, maskExtStripped, extWCP))
              
return true;
         }
        }
        
else
        {
        
// ext is not masked, requires exact ext
                  // and ck for fn masking

        
if (maskName.IndexOf(asterisk) > -1)
         {
            
// fn masked
            
if (IsValueMatch(fn, maskNameStripped, fnWCP) &&
                        ext.Equals(maskExtStripped))
              
return true;
            
else if (fn.Equals(maskName) &&
                              ext.Equals(maskExtStripped))
              
return true;
         }
        }
      }
      
return false;
   }

  
/// <summary>
  
/// Returns position of existing wildcard[s].
  
/// start, wrap, end, none
  
/// </summary>
  
/// <param name = "mask"></param>
  
/// <returns>wcPos</returns>
  
private wcPos GetWildCardPosition(string mask)
   {
      
string wildStart = "^\\*\\w+";
      
string wildEnd = "^[a-z0-9_ ]*\\*";
      
string wildWrap = "^\\*[a-z0-9_ ]*\\*";
      
if (mask.Equals("*"))
        
return wcPos.none;
      
else if (Regex.IsMatch(mask, wildWrap))
        
return wcPos.wrap;
      
else if (Regex.IsMatch(mask, wildStart))
        
return wcPos.start;
      
else if (Regex.IsMatch(mask, wildEnd))
        
return wcPos.@end;
      
else
        
return wcPos.none;
   }


  
/// <summary>
  
/// Returns true if mask matches value, based on position specifier.
  
/// </summary>
  
/// <param name = "value"></param>
  
/// <param name = "mask"></param>
  
/// <returns>Boolean</returns>
  
private bool IsValueMatch(string valu, string mask, wcPos position)
   {
      
switch (position)
      {
        
case wcPos.wrap:
        
if (valu.IndexOf(mask) > -1)
          
return true;
        
break;
        
case wcPos.@end:
        
if (valu.StartsWith(mask))
          
return true;
        
break;
        
case wcPos.start:
        
if (valu.EndsWith(mask))
          
return true;
        
break;
      }
      
return false;
   }


Ask a Question, or give your feedback on my articles or products by going to the KnowDotNet Forum or by clicking on My Blog.