Jonathan Jenne 0fa654bda0 clean up
2022-01-21 15:51:14 +01:00

413 lines
17 KiB
VB.net

Imports System.IO
Imports System.Security.Cryptography
Imports System.Text
Imports System.Text.RegularExpressions
Imports DigitalData.Modules.Logging
''' <module>File</module>
''' <version>0.0.0.1</version>
''' <date>11.10.2018</date>
''' <summary>
''' Module that provides variouse File operations
''' </summary>
''' <dependencies>
''' NLog, >= 4.5.8
''' </dependencies>
''' <params>
''' LogConfig, DigitalData.Module.Logging.LogConfig
''' A LogConfig object
''' </params>
''' <props>
''' </props>
''' <example>
''' </example>
''' <remarks>
''' </remarks>
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
''' <summary>
''' Reads the file at `FilePath` and computes a SHA256 Hash from its contents
''' </summary>
''' <param name="FilePath"></param>
''' <returns></returns>
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
''' <summary>
''' Adds file version string to given filename `Destination` if that file already exists.
''' </summary>
''' <param name="Destination"></param>
''' <returns></returns>
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 oFileVersion As Integer = Nothing
' Split Filename without extension at version separator to:
' - Check if file is already versioned
' - Get the file version of an already versioned file
'
' Example:
' 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
Dim oFileNameWithoutExtension = Path.GetFileNameWithoutExtension(oFileName)
Dim oSplitFilename = oFileNameWithoutExtension.Split(oVersionSeparator).ToList()
' if file is already versioned, extract file version
' else just use the filename and set version to 1
If oSplitFilename.Count > 1 Then
Dim oVersion As Integer = 1
Try
oVersion = Integer.Parse(oSplitFilename.Last())
oFileNameWithoutExtension = String.Join("", oSplitFilename.Take(oSplitFilename.Count - 1))
Catch ex As Exception
' oFilenameWithoutExtension does NOT change
oFileNameWithoutExtension = oFileNameWithoutExtension
Finally
oFileVersion = oVersion
End Try
Else
oFileVersion = 1
End If
' 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.
If oFileName.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
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
''' <summary>
''' Removes files in a directory filtered by filename, extension and last write date
''' </summary>
''' <param name="Path">The directory in which files will be deleted</param>
''' <param name="FileKeepTime">Only delete files which are older than x days. Must be between 0 and 1000 days.</param>
''' <param name="FileBaseName">A filename filter which will be checked</param>
''' <param name="FileExtension">A file extension which will be checked</param>
''' <param name="ContinueOnError">Should the function continue with deleting when a file could not be deleted?</param>
''' <returns>True if all files were deleted or if no files were deleted, otherwise false</returns>
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
<DebuggerStepThrough>
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
<DebuggerStepThrough>
Public Sub MoveTo(FilePath As String, NewFileName As String, Directory As String)
IO.File.Move(FilePath, Path.Combine(Directory, NewFileName))
End Sub
''' <summary>
''' Copied from https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories
''' </summary>
''' <param name="SourceDirName"></param>
''' <param name="DestDirName"></param>
''' <param name="CopySubDirs"></param>
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
''' <summary>
''' Tries to create a directory and returns its path.
''' Returns a temp path if `DirectoryPath` can not be created or written to.
''' </summary>
''' <param name="DirectoryPath">The directory to create</param>
''' <param name="TestWriteAccess">Should a write access test be performed?</param>
''' <returns>The used path</returns>
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
''' <summary>
''' Checks if a file is locked, ie. in use by another process.
''' </summary>
''' <remarks>
''' https://docs.microsoft.com/en-us/dotnet/standard/io/handling-io-errors
''' </remarks>
Public Function TestFileIsLocked(pFilePath As String) As Boolean
Try
Using stream As FileStream = IO.File.Open(FileMode.Open, FileAccess.Read, FileShare.None)
stream.Close()
End Using
Catch ex As Exception When ((ex.HResult And &HFFFF) = 32)
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) As String
Dim oDateDirectory = GetDateString()
Dim oFinalDirectory As String = IO.Path.Combine(pBaseDirectory, oDateDirectory)
Return oFinalDirectory
End Function
Public Function CreateDateDirectory(pBaseDirectory As String) As String
Dim oDateDirectory = GetDateString()
Dim oFinalDirectory As String = IO.Path.Combine(pBaseDirectory, oDateDirectory)
If IO.Directory.Exists(oFinalDirectory) = False Then
Try
Directory.CreateDirectory(oFinalDirectory)
Catch ex As Exception
_Logger.Error(ex)
End Try
End If
Return oFinalDirectory
End Function
Public Function GetDateString() As String
Return $"{Now:yyyy\\MM\\dd}"
End Function
Public Function GetDateTimeString() As String
Return $"{Now: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
End Class