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 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 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 ''' ''' 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 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 End Class