Imports System.IO
Imports System.Security.Cryptography
Imports System.Text
Imports System.Text.RegularExpressions
Imports DigitalData.Modules.Logging
''' File
''' 0.0.0.1
''' 11.10.2018
'''
''' Module that provides variouse File operations
'''
'''
''' NLog, >= 4.5.8
'''
'''
''' LogConfig, DigitalData.Module.Logging.LogConfig
''' A LogConfig object
'''
'''
'''
'''
'''
'''
'''
Public Class File
Private ReadOnly _Logger As Logger
Private ReadOnly _LogConfig As LogConfig
Private ReadOnly _invalidFilenameChars As String
Private ReadOnly _invalidPathChars As String
Private Const REGEX_CLEAN_FILENAME As String = "[\\/:""<>|\b\0\r\n\t]"
Private Const REGEX_CLEAN_PATH As String = "[""<>|\b\0\r\n\t]"
' The limit enforced by windows for filenpaths is 260,
' so we use a slightly smaller number to have some Error margin.
'
' Source: https://docs.microsoft.com/de-de/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#maximum-path-length-limitation
Private Const MAX_FILE_PATH_LENGTH = 250
Private Const FILE_NAME_ACCESS_TEST = "accessTest.txt"
Public Sub New(LogConfig As LogConfig)
_LogConfig = LogConfig
_Logger = LogConfig.GetLogger()
_invalidFilenameChars = String.Join("", Path.GetInvalidFileNameChars())
_invalidPathChars = String.Join("", Path.GetInvalidPathChars())
End Sub
Public Function GetCleanFilename(FileName As String) As String
_Logger.Debug("Filename before cleaning: [{0}]", FileName)
Dim oCleanName As String = FileName
oCleanName = Regex.Replace(oCleanName, _invalidFilenameChars, String.Empty)
oCleanName = Regex.Replace(oCleanName, REGEX_CLEAN_FILENAME, String.Empty, RegexOptions.Singleline)
oCleanName = Regex.Replace(oCleanName, "\s{2,}", " ")
oCleanName = Regex.Replace(oCleanName, "\.{2,}", ".")
_Logger.Debug("Filename after cleaning: [{0}]", oCleanName)
Return oCleanName
End Function
Public Function GetCleanPath(FilePath As String) As String
_Logger.Debug("Path before cleaning: [{0}]", FilePath)
Dim oCleanName As String = FilePath
oCleanName = Regex.Replace(oCleanName, _invalidPathChars, String.Empty)
oCleanName = Regex.Replace(oCleanName, REGEX_CLEAN_PATH, String.Empty, RegexOptions.Singleline)
_Logger.Debug("Path after cleaning: [{0}]", oCleanName)
Return oCleanName
End Function
'''
''' Reads the file at `FilePath` and computes a SHA256 Hash from its contents
'''
'''
'''
Public Function GetChecksum(FilePath As String) As String
Try
Using oFileStream = IO.File.OpenRead(FilePath)
Using oStream As New BufferedStream(oFileStream, 1200000)
Dim oChecksum() As Byte = SHA256.Create.ComputeHash(oStream)
Return FormatHash(oChecksum)
End Using
End Using
Catch ex As Exception
_Logger.Error(ex)
Return Nothing
End Try
End Function
Public Function GetChecksumFromString(pStringToCheck As String) As String
Dim oBytes() As Byte = Encoding.UTF8.GetBytes(pStringToCheck)
Dim oChecksum() As Byte = SHA256.Create.ComputeHash(oBytes)
Return FormatHash(oChecksum)
End Function
Private Function FormatHash(pChecksum)
Return BitConverter.
ToString(pChecksum).
Replace("-", String.Empty)
End Function
'''
''' Adds file version string to given filename `Destination` if that file already exists.
'''
'''
'''
Public Function GetVersionedFilename(Destination As String) As String
Try
Dim oFileName As String = Destination
Dim oFinalFileName = oFileName
Dim oDestinationDir = Path.GetDirectoryName(oFileName)
Dim oExtension = Path.GetExtension(oFileName)
Dim oVersionSeparator As Char = "~"c
Dim oFileNameWithoutExtension = Path.GetFileNameWithoutExtension(oFileName)
Dim oSplitResult = GetVersionedString(oFileNameWithoutExtension, oVersionSeparator)
oFileNameWithoutExtension = oSplitResult.Item1
Dim oFileVersion = oSplitResult.Item2
' Shorten the filename (only filename, without extension or version)
' by cutting the length in half. This should work no matter how long the path and/or filename are.
' The initial check operates on the full path to catch all scenarios.
If Destination.Length > MAX_FILE_PATH_LENGTH Then
_Logger.Info("Filename is too long. Filename will be cut to prevent further errors.")
_Logger.Info("Original Filename is: {0}", oFileNameWithoutExtension)
Dim oNewLength As Integer = Math.Round(oFileNameWithoutExtension.Length / 2)
Dim oNewFileNameWithoutExtension = oFileNameWithoutExtension.Substring(0, oNewLength)
_Logger.Info("New Filename will be: {0}", oNewFileNameWithoutExtension)
oFileNameWithoutExtension = oNewFileNameWithoutExtension
End If
' while file exists, increment version
Do
oFinalFileName = Path.Combine(oDestinationDir, GetFilenameWithVersion(oFileNameWithoutExtension, oVersionSeparator, oFileVersion, oExtension))
_Logger.Debug("Intermediate Filename is {0}", oFinalFileName)
_Logger.Debug("File version: {0}", oFileVersion)
oFileVersion += 1
Loop While (IO.File.Exists(oFinalFileName))
_Logger.Debug("Final Filename is {0}", oFinalFileName)
Return oFinalFileName
Catch ex As Exception
_Logger.Warn("Filename {0} could not be versioned. Original filename will be returned!", Destination)
_Logger.Error(ex)
Return Destination
End Try
End Function
'''
''' Split String at version separator to:
''' check if string is already versioned,
''' get the string version of an already versioned string
'''
'''
''' Examples:
''' test1.pdf --> test1 --> ['test1'] --> no fileversion
''' test1~2.pdf --> test1~2 --> ['test1', '2'] --> version 2
''' test1~12345~2.pdf --> test1~12345~2 --> ['test1', '12345', '2'] --> still version 2
''' somestring~3 --> somestring~3 --> ['somestring', '3'] --> version 3
'''
''' The string to versioned
''' The character to split at
''' Tuple of string and version
Public Function GetVersionedString(pString As String, pSeparator As Char) As Tuple(Of String, Integer)
Dim oSplitString = pString.Split(pSeparator).ToList()
Dim oStringVersion As Integer
' if string is already versioned, extract string version
' else just use the string and set version to 1
If oSplitString.Count > 1 Then
Dim oVersion As Integer = 1
Try
oVersion = Integer.Parse(oSplitString.Last())
pString = String.Join("", oSplitString.Take(oSplitString.Count - 1))
Catch ex As Exception
' pString does NOT change
pString = pString
Finally
oStringVersion = oVersion
End Try
Else
oStringVersion = 1
End If
Return New Tuple(Of String, Integer)(pString, oStringVersion)
End Function
Public Function GetAppDataPath(CompanyName As String, ProductName As String)
Dim oLocalAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
Return Path.Combine(oLocalAppData, CompanyName, ProductName)
End Function
Private Function GetFilenameWithVersion(FileNameWithoutExtension As String, VersionSeparator As Char, FileVersion As Integer, Extension As String) As String
If FileVersion <= 1 Then
Return $"{FileNameWithoutExtension}{Extension}"
Else
Return $"{FileNameWithoutExtension}{VersionSeparator}{FileVersion}{Extension}"
End If
End Function
'''
''' Removes files in a directory filtered by filename, extension and last write date
'''
''' The directory in which files will be deleted
''' Only delete files which are older than x days. Must be between 0 and 1000 days.
''' A filename filter which will be checked
''' A file extension which will be checked
''' Should the function continue with deleting when a file could not be deleted?
''' True if all files were deleted or if no files were deleted, otherwise false
Public Function RemoveFiles(Path As String, FileKeepTime As Integer, FileBaseName As String, Optional FileExtension As String = "log", Optional ContinueOnError As Boolean = True) As Boolean
If Not TestPathIsDirectory(Path) Then
Throw New ArgumentException($"Path {Path} is not a directory!")
End If
If Not Directory.Exists(Path) Then
Throw New DirectoryNotFoundException($"Path {Path} does not exist!")
End If
If FileKeepTime < 0 Or FileKeepTime > 1000 Then
Throw New ArgumentOutOfRangeException("FileKeepTime must be an integer between 0 and 1000!")
End If
Dim oUnableToDeleteCounter = 0
Dim oDirectory As New DirectoryInfo(Path)
Dim oDateLimit As DateTime = DateTime.Now.AddDays(FileKeepTime)
Dim oFiles As List(Of FileInfo) = oDirectory.
EnumerateFiles($"*{FileBaseName}*").
Where(Function(oFileInfo As FileInfo)
Return oFileInfo.Extension = FileExtension And oFileInfo.LastWriteTime < oDateLimit
End Function).
ToList()
If oFiles.Count = 0 Then
_Logger.Debug("No files found that match the criterias.")
Return True
End If
_Logger.Debug("Deleting old files (Found {0}).", oFiles.Count)
For Each oFile As FileInfo In oFiles
Try
oFile.Delete()
Catch ex As Exception
If ContinueOnError = False Then
_Logger.Warn("Deleting files was aborted at file {0}.", oFile.FullName)
Return False
End If
oUnableToDeleteCounter = oUnableToDeleteCounter + 1
_Logger.Warn("File {0} could not be deleted!")
End Try
Next
If oUnableToDeleteCounter > 0 Then
_Logger.Debug("Old files partially removed. {0} files could not be removed.", oUnableToDeleteCounter)
Else
_Logger.Debug("Old files removed.")
End If
Return True
End Function
Public Sub MoveTo(FilePath As String, Directory As String)
Dim oFileInfo As New FileInfo(FilePath)
IO.File.Move(FilePath, Path.Combine(Directory, oFileInfo.Name))
End Sub
Public Sub MoveTo(FilePath As String, NewFileName As String, Directory As String)
IO.File.Move(FilePath, Path.Combine(Directory, NewFileName))
End Sub
'''
''' Copied from https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories
'''
'''
'''
'''
Public Sub CopyDirectory(ByVal SourceDirName As String, ByVal DestDirName As String, ByVal CopySubDirs As Boolean)
Dim oDirectory As DirectoryInfo = New DirectoryInfo(SourceDirName)
If Not oDirectory.Exists Then
Throw New DirectoryNotFoundException("Source directory does not exist or could not be found: " & SourceDirName)
End If
Dim oDirectories As DirectoryInfo() = oDirectory.GetDirectories()
Directory.CreateDirectory(DestDirName)
Dim oFiles As FileInfo() = oDirectory.GetFiles()
For Each oFile As FileInfo In oFiles
Dim tempPath As String = Path.Combine(DestDirName, oFile.Name)
oFile.CopyTo(tempPath, False)
Next
If CopySubDirs Then
For Each oSubDirectory As DirectoryInfo In oDirectories
Dim oTempPath As String = Path.Combine(DestDirName, oSubDirectory.Name)
CopyDirectory(oSubDirectory.FullName, oTempPath, CopySubDirs)
Next
End If
End Sub
'''
''' Tries to create a directory and returns its path.
''' Returns a temp path if `DirectoryPath` can not be created or written to.
'''
''' The directory to create
''' Should a write access test be performed?
''' The used path
Public Function CreateDirectory(DirectoryPath As String, Optional TestWriteAccess As Boolean = True) As String
Dim oFinalPath As String
If Directory.Exists(DirectoryPath) Then
_Logger.Debug("Directory {0} already exists. Skipping.", DirectoryPath)
oFinalPath = DirectoryPath
Else
Try
Directory.CreateDirectory(DirectoryPath)
oFinalPath = DirectoryPath
Catch ex As Exception
_Logger.Error(ex)
_Logger.Warn("Directory {0} could not be created. Temp path will be used instead.", DirectoryPath)
oFinalPath = Path.GetTempPath()
End Try
End If
If TestWriteAccess AndAlso Not TestPathIsWritable(DirectoryPath) Then
_Logger.Warn("Directory {0} is not writable. Temp path will be used instead.", DirectoryPath)
oFinalPath = Path.GetTempPath()
Else
oFinalPath = DirectoryPath
End If
_Logger.Debug("Using path {0}", oFinalPath)
Return oFinalPath
End Function
Public Function TestPathIsWritable(DirectoryPath As String) As Boolean
Try
Dim fileAccessPath = Path.Combine(DirectoryPath, FILE_NAME_ACCESS_TEST)
Using fs As FileStream = IO.File.Create(fileAccessPath)
fs.WriteByte(0)
End Using
IO.File.Delete(fileAccessPath)
Return True
Catch ex As Exception
Return False
End Try
End Function
'''
''' Checks if a file is locked, ie. in use by another process.
'''
'''
''' https://docs.microsoft.com/en-us/dotnet/standard/io/handling-io-errors
''' https://stackoverflow.com/questions/876473/is-there-a-way-to-check-if-a-file-is-in-use
'''
Public Function TestFileIsLocked(pFilePath As String) As Boolean
Try
Using stream As FileStream = IO.File.Open(pFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)
stream.Close()
End Using
Catch ex As Exception When ((ex.HResult And &HFFFF) = 32)
Return True
Catch ex As Exception
Return True
End Try
Return False
End Function
Public Function TestPathIsDirectory(Path As String) As Boolean
If Not Directory.Exists(Path) Then
Return False
End If
Dim oIsDirectory As Boolean = (System.IO.File.GetAttributes(Path) And FileAttributes.Directory) = FileAttributes.Directory
Return oIsDirectory
End Function
Public Function GetDateDirectory(pBaseDirectory As String, pDate As Date) As String
Dim oDateDirectory = GetDateString(pDate)
Dim oFinalDirectory As String = IO.Path.Combine(pBaseDirectory, oDateDirectory)
Return oFinalDirectory
End Function
Public Function GetDateDirectory(pBaseDirectory As String) As String
Return GetDateDirectory(pBaseDirectory, Now)
End Function
Public Function CreateDateDirectory(pBaseDirectory As String, pDate As Date) As String
Dim oDateDirectory = GetDateString(pDate)
Dim oFinalDirectory As String = IO.Path.Combine(pBaseDirectory, oDateDirectory)
If IO.Directory.Exists(oFinalDirectory) = False Then
_Logger.Debug("Path does not exist, creating: [{0}]", oFinalDirectory)
Try
Directory.CreateDirectory(oFinalDirectory)
_Logger.Debug("Created folder [{0}]", oFinalDirectory)
Catch ex As Exception
_Logger.Warn("Final path [{0}] could not be created!", oFinalDirectory)
_Logger.Error(ex)
End Try
End If
Return oFinalDirectory
End Function
Public Function CreateDateDirectory(pBaseDirectory As String) As String
Return CreateDateDirectory(pBaseDirectory, Now)
End Function
Public Function GetDateString() As String
Return $"{Now:yyyy\\MM\\dd}"
End Function
Public Function GetDateString(pDate As Date) As String
Return $"{pDate:yyyy\\MM\\dd}"
End Function
Public Function GetDateTimeString() As String
Return $"{Now:yyyy-MM-dd_hh-mm-ffff}"
End Function
Public Function GetDateTimeString(pDate As Date) As String
Return $"{pDate:yyyy-MM-dd_hh-mm-ffff}"
End Function
Public Function GetFilenameWithSuffix(pFilePath As String, pSuffix As String)
Dim oFileInfo = New IO.FileInfo(pFilePath)
Return GetFilenameWithSuffix(IO.Path.GetFileNameWithoutExtension(pFilePath), pSuffix, oFileInfo.Extension.Substring(1))
End Function
Public Function GetFilenameWithSuffix(pBaseString As String, pSuffix As String, pExtension As String)
Return $"{pBaseString}-{pSuffix}.{pExtension}"
End Function
Public Function GetFilenameWithPrefix(pFilePath As String, pPrefix As String)
Dim oFileInfo = New IO.FileInfo(pFilePath)
Return GetFilenameWithSuffix(IO.Path.GetFileNameWithoutExtension(pFilePath), pPrefix, oFileInfo.Extension.Substring(1))
End Function
Public Function GetFilenameWithPrefix(pBaseString As String, pPrefix As String, pExtension As String)
Return $"{pPrefix}-{pBaseString}.{pExtension}"
End Function
End Class