diff --git a/Base/Base.vbproj b/Base/Base.vbproj index 30cb4ceb..170d8af0 100644 --- a/Base/Base.vbproj +++ b/Base/Base.vbproj @@ -83,6 +83,13 @@ + + + + + + + @@ -139,8 +146,6 @@ Logging - - - + \ No newline at end of file diff --git a/Base/FileContainer/DocumentObject.vb b/Base/FileContainer/DocumentObject.vb new file mode 100644 index 00000000..c9da13f0 --- /dev/null +++ b/Base/FileContainer/DocumentObject.vb @@ -0,0 +1,18 @@ +Imports System.Runtime.Serialization + + +Public Class DocumentObject + + + Public ReadOnly FileName As String + + Public ReadOnly ContainerId As String + + Public ReadOnly DocumentId As Int64 + + Public Sub New(ContainerId As String, DocumentId As Int64, FileName As String) + Me.ContainerId = ContainerId + Me.DocumentId = DocumentId + Me.FileName = FileName + End Sub +End Class diff --git a/Base/FileContainer/FileContainer.vb b/Base/FileContainer/FileContainer.vb new file mode 100644 index 00000000..54d6ce4a --- /dev/null +++ b/Base/FileContainer/FileContainer.vb @@ -0,0 +1,193 @@ +Imports System.IO +Imports DigitalData.Modules.Logging +Imports DigitalData.Modules.Encryption +Imports ProtoBuf + +''' FileContainer +''' 0.0.0.2 +''' 21.11.2018 +''' +''' File Container for securely saving files +''' +''' +''' NLog, >= 4.5.8 +''' +''' +''' LogConfig, DigitalData.Module.Logging.LogConfig +''' A LogConfig object +''' Password, String +''' The Password to Encrypt +''' Path, String +''' The Path to save/load the container +''' +''' +''' dim oContainer = Container.Create(logConfig, "pass", "E:\some.container") +''' dim oContainer = Container.Load(logConfig, "pass", "E:\some.container") +''' +''' dim oContainer = new Container(logConfig, "pass", "E:\some.container") +''' oContainer.Save() +''' +''' dim oContainer = new Container(logConfig, "pass", "E:\some.container") +''' oContainer.Contents = oSomeData +''' oContainer.Save() +''' +''' dim oContainer = new Container(logConfig, "pass", "E:\some.container") +''' oContainer.Load() +''' dim oContents = oContainer.Contents +''' +''' dim oContainer = new Container(logConfig, "pass", "E:\some.container") +''' oContainer.Load() +''' oContainer.Contents = oSomeOtherData +''' oContainer.Save() +''' oContainer.SaveAs("E:\some2.container") +''' +Public Class FileContainer + Private _crypto As Encryption.Encryption + Private _compression As Compression + Private _inner As FileContainerInner + Private _logger As Logger + Private _logConfig As LogConfig + Private _path As String + + Public Property Contents As Byte() + Get + Return _inner.Contents + End Get + Set(value As Byte()) + _inner.Contents = value + End Set + End Property + Public ReadOnly Property ContainerId As String + Get + Return _inner.FileId + End Get + End Property + Public ReadOnly Property CreatedAt As String + Get + Return _inner.CreatedAt + End Get + End Property + Public ReadOnly Property UpdatedAt As String + Get + Return _inner.UpdatedAt + End Get + End Property + + Public Shared Function Create(LogConfig As LogConfig, Password As String) As FileContainer + Dim oContainer = New FileContainer(LogConfig, Password) + Return oContainer + End Function + + Public Shared Function Load(LogConfig As LogConfig, Password As String, Path As String) As FileContainer + Dim oContainer = New FileContainer(LogConfig, Password, Path) + oContainer.Load() + Return oContainer + End Function + + Public Sub New(LogConfig As LogConfig, Password As String) + _logger = LogConfig.GetLogger() + _crypto = New Encryption.Encryption(LogConfig, Password) + _compression = New Compression(LogConfig) + _inner = New FileContainerInner() + End Sub + + Public Sub New(LogConfig As LogConfig, Password As String, Path As String) + MyClass.New(LogConfig, Password) + _path = Path + End Sub + + Public Sub SetFile(Contents As Byte(), FileName As String) + _inner.Contents = Contents + _inner.UpdatedAt = Date.Now + _inner.FileName = FileName + End Sub + + Public Function GetFile() As FileContainerInner + Return _inner + End Function + + Public Sub Save() + If IsNothing(_path) Then + Throw New ArgumentException("Path not set") + End If + + SaveAs(_path) + End Sub + + Public Sub SaveAs(Path As String) + Try + WriteBytesToFile(TransformToBytes(_inner), Path) + Catch ex As Exception + _logger.Error(ex) + Throw ex + End Try + End Sub + + Public Sub Load() + If IsNothing(_path) Then + Throw New ArgumentException("Path not set") + End If + + LoadFrom(_path) + End Sub + + Public Sub LoadFrom(Path As String) + Try + _inner = TransformToObject(ReadBytesFromFile(_path)) + Catch ex As Exception + _logger.Error(ex) + Throw ex + End Try + End Sub + + Private Function TransformToBytes([Object] As FileContainerInner) As Byte() + Dim oBytes = Serialize([Object]) + Dim oCompressed = _compression.Compress(oBytes) + Dim oEncrypted = _crypto.Encrypt(oCompressed) + Return oEncrypted + End Function + + Private Function TransformToObject(Bytes As Byte()) As FileContainerInner + Dim oDecrypted = _crypto.Decrypt(Bytes) + Dim oDecompressed = _compression.Decompress(oDecrypted) + Dim oObject = Deserialize(oDecompressed) + Return oObject + End Function + + Private Function Serialize(InnerData As FileContainerInner) As Byte() + Dim oBinaryData As Byte() + + Using oStream As New MemoryStream + Serializer.Serialize(oStream, InnerData) + oBinaryData = oStream.ToArray() + End Using + + Return oBinaryData + End Function + + Private Function Deserialize(InnerData As Byte()) As FileContainerInner + Dim oObject As FileContainerInner + + Using oStream As New MemoryStream(InnerData) + oObject = Serializer.Deserialize(Of FileContainerInner)(oStream) + End Using + + Return oObject + End Function + + Private Sub WriteBytesToFile(Data As Byte(), FilePath As String) + Using oSourceStream As New FileStream(FilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None) + oSourceStream.Write(Data, 0, Data.Length) + oSourceStream.Flush() + End Using + End Sub + + Private Function ReadBytesFromFile(FilePath As String) As Byte() + Using oFileStream = New FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096) + Dim oBuffer As Byte() = New Byte(oFileStream.Length - 1) {} + oFileStream.Read(oBuffer, 0, oFileStream.Length) + oFileStream.Close() + Return oBuffer + End Using + End Function +End Class diff --git a/Base/FileContainer/FileContainerInner.vb b/Base/FileContainer/FileContainerInner.vb new file mode 100644 index 00000000..fc787ce0 --- /dev/null +++ b/Base/FileContainer/FileContainerInner.vb @@ -0,0 +1,23 @@ +Imports ProtoBuf + + + +Public Class FileContainerInner + + Public FileId As String + + Public Contents As Byte() + + Public CreatedAt As DateTime + + Public UpdatedAt As DateTime + + Public FileName As String + + Public Sub New() + FileId = Guid.NewGuid().ToString + CreatedAt = Date.Now + UpdatedAt = Date.Now + End Sub + +End Class \ No newline at end of file diff --git a/Base/FileWatcher/FileWatcher.vb b/Base/FileWatcher/FileWatcher.vb new file mode 100644 index 00000000..6f82047a --- /dev/null +++ b/Base/FileWatcher/FileWatcher.vb @@ -0,0 +1,132 @@ +Imports System.IO +Imports DigitalData.Modules.Filesystem +Imports DigitalData.Modules.Filesystem.FileWatcherFilters +Imports DigitalData.Modules.Logging + + +Public Class FileWatcher + ' Internals + Private ReadOnly _Logger As Logger + Private ReadOnly _Watchers As List(Of FileSystemWatcher) + Private ReadOnly _Files As Dictionary(Of String, FileWatcherProperties) + Private ReadOnly _Filters As List(Of BaseFileFilter) + + ' Options + Private _Path As String + + ' Public Events + Public Event FileSaved(ByVal FullName As String, ByVal IsSpecial As Boolean) + + Public Sub New(LogConfig As LogConfig, Path As String, Optional Filters As List(Of BaseFileFilter) = Nothing) + _Logger = LogConfig.GetLogger() + _Files = New Dictionary(Of String, FileWatcherProperties) + _Watchers = New List(Of FileSystemWatcher) + _Filters = IIf(IsNothing(Filters), GetDefaultFilters(), Filters) + _Path = Path + + For Each oFilePath In Directory.EnumerateFiles(_Path) + Try + If IO.File.Exists(oFilePath) Then + _Files.Add(oFilePath, New FileWatcherProperties With { + .CreatedAt = DateTime.Now, + .ChangedAt = Nothing + }) + End If + Catch ex As Exception + _Logger.Error(ex) + _Logger.Warn("File {0} cannot be watched!") + End Try + Next + End Sub + + Public Sub Add(Filter As String) + _Watchers.Add(CreateWatcher(Filter)) + End Sub + + Public Sub Start() + For Each oWatcher In _Watchers + oWatcher.EnableRaisingEvents = True + Next + End Sub + + Public Sub [Stop]() + For Each oWatcher In _Watchers + If Not IsNothing(oWatcher) Then + oWatcher.EnableRaisingEvents = False + oWatcher.Dispose() + End If + Next + End Sub + + Private Function GetDefaultFilters() + Return New List(Of BaseFileFilter) From { + New TempFileFilter, + New OfficeFileFilter + } + End Function + + Private Function CreateWatcher(Filter As String) + Dim oWatcher = New FileSystemWatcher() With { + .Path = _Path, + .Filter = Filter, + .NotifyFilter = NotifyFilters.LastAccess _ + Or NotifyFilters.LastWrite _ + Or NotifyFilters.FileName _ + Or NotifyFilters.Size _ + Or NotifyFilters.FileName _ + Or NotifyFilters.Attributes + } + + AddHandler oWatcher.Created, AddressOf HandleFileCreated + AddHandler oWatcher.Changed, AddressOf HandleFileChanged + AddHandler oWatcher.Deleted, AddressOf HandleFileDeleted + AddHandler oWatcher.Renamed, AddressOf HandleFileRenamed + + Return oWatcher + End Function + + Private Sub HandleFileCreated(sender As Object, e As FileSystemEventArgs) + _Files.Add(e.FullPath, New FileWatcherProperties()) + _Logger.Debug("[Created] " & e.FullPath) + End Sub + + ''' + ''' This may fire twice for a single save operation, + ''' see: https://blogs.msdn.microsoft.com/oldnewthing/20140507-00/?p=1053/ + ''' + Private Sub HandleFileChanged(sender As Object, e As FileSystemEventArgs) + _Files.Item(e.FullPath).ChangedAt = DateTime.Now + _Logger.Debug("[Changed] " & e.FullPath) + + Dim oShouldRaiseSave As Boolean = Not _Filters.Any(Function(oFilter) + Return oFilter.ShouldFilter(e) + End Function) + + If oShouldRaiseSave Then + RaiseEvent FileSaved(e.FullPath, False) + End If + End Sub + Private Sub HandleFileDeleted(sender As Object, e As FileSystemEventArgs) + _Files.Remove(e.FullPath) + _Logger.Debug("[Removed] " & e.FullPath) + End Sub + + Private Sub HandleFileRenamed(sender As Object, e As RenamedEventArgs) + Dim oProperties = _Files.Item(e.OldFullPath) + _Files.Remove(e.OldFullPath) + _Files.Add(e.FullPath, oProperties) + ' Soll eine umbenannte datei als NEU gelten? + + Dim oShouldRaiseSave = _Filters.Any(Function(oFilter) + Return oFilter.ShouldRaiseSave(e) + End Function) + + If oShouldRaiseSave Then + RaiseEvent FileSaved(e.OldFullPath, True) + End If + + _Logger.Debug("[Renamed] {0} --> {1}", e.OldFullPath, e.FullPath) + End Sub + + +End Class diff --git a/Base/FileWatcher/FileWatcherFilters.vb b/Base/FileWatcher/FileWatcherFilters.vb new file mode 100644 index 00000000..84f70b62 --- /dev/null +++ b/Base/FileWatcher/FileWatcherFilters.vb @@ -0,0 +1,61 @@ +Imports System.IO + +''' +''' Built-in filters for FileWatcher that are useful for correctly detecting changes on Office documents (currently Office 2016) +''' +Public Class FileWatcherFilters + ''' + ''' Base Filter that all filters must inherit from + ''' Provides two functions that may be overridden and some useful file extension lists + ''' + Public MustInherit Class BaseFileFilter + Public TempFiles As New List(Of String) From {".tmp", ""} + + Public Overridable Function ShouldFilter(e As FileSystemEventArgs) As Boolean + Return False + End Function + Public Overridable Function ShouldRaiseSave(e As RenamedEventArgs) As Boolean + Return False + End Function + End Class + + ''' + ''' Simple Filter that filters changes made on temporary files + ''' + Public Class TempFileFilter + Inherits BaseFileFilter + + Public Overrides Function ShouldFilter(e As FileSystemEventArgs) As Boolean + Dim oFileInfo As New FileInfo(e.FullPath) + Return TempFiles.Contains(oFileInfo.Extension) + End Function + End Class + + ''' + ''' Filter to detect changes on Office files + ''' + Public Class OfficeFileFilter + Inherits BaseFileFilter + + Public OfficeFiles As New List(Of String) From {".docx", ".pptx", ".xlsx"} + + Public Overrides Function ShouldFilter(e As FileSystemEventArgs) As Boolean + Dim oFileInfo As New FileInfo(e.FullPath) + Return OfficeFiles.Contains(oFileInfo.Extension) And oFileInfo.Name.StartsWith("~") + End Function + + Public Overrides Function ShouldRaiseSave(e As RenamedEventArgs) As Boolean + Dim oIsTransform = OfficeFiles.Any(Function(Extension As String) + Return e.OldName.EndsWith(Extension) + End Function) + + ' Check if it is renamed to a temp file + Dim oIsTempFile = TempFiles.Any(Function(Extension) + Return e.Name.EndsWith(Extension) + End Function) + + + Return oIsTransform And oIsTempFile + End Function + End Class +End Class diff --git a/Base/FileWatcher/FileWatcherProperties.vb b/Base/FileWatcher/FileWatcherProperties.vb new file mode 100644 index 00000000..5eadecbe --- /dev/null +++ b/Base/FileWatcher/FileWatcherProperties.vb @@ -0,0 +1,10 @@ +Public Class FileWatcherProperties + Public Property CreatedAt As DateTime + Public Property ChangedAt As DateTime + Public ReadOnly Property HasChanged As Boolean + Public Sub New() + CreatedAt = DateTime.Now + ChangedAt = Nothing + HasChanged = False + End Sub +End Class diff --git a/Base/FilesystemEx.vb b/Base/FilesystemEx.vb new file mode 100644 index 00000000..a746d93f --- /dev/null +++ b/Base/FilesystemEx.vb @@ -0,0 +1,495 @@ +Imports DigitalData.Modules.Logging +Imports System.IO +Imports System.Security.Cryptography +Imports System.Text +Imports System.Text.RegularExpressions + +Public Class FilesystemEx + 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 + + ' This prevents an infinite loop when no file can be created in a location + Private Const MAX_FILE_VERSION = 100 + Private Const VERSION_SEPARATOR As Char = "~"c + + 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 + + Public Function GetHash(FilePath As String) As String + Return GetChecksum(FilePath) + End Function + + Public Function GetHashFromString(pStringToCheck As String) As String + Return GetChecksumFromString(pStringToCheck) + 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. + ''' + ''' Filepath to check + ''' Versioned string + Public Function GetVersionedFilename(pFilePath As String) As String + Return GetVersionedFilenameWithFilecheck(pFilePath, Function(pPath As String) IO.File.Exists(pPath)) + End Function + + ''' + ''' Adds file version string to given filename `Destination` if that file already exists. + ''' + ''' Filepath to check + ''' Custom action to check for file existence + ''' Versioned string + Public Function GetVersionedFilenameWithFilecheck(pFilePath As String, pFileExistsAction As Func(Of String, Boolean)) As String + Try + Dim oFileName As String = pFilePath + Dim oFinalFileName = oFileName + + Dim oDestinationDir = Path.GetDirectoryName(oFileName) + Dim oExtension = Path.GetExtension(oFileName) + + Dim oFileNameWithoutExtension = Path.GetFileNameWithoutExtension(oFileName) + Dim oSplitResult = GetVersionedString(oFileNameWithoutExtension) + + oFileNameWithoutExtension = oSplitResult.Item1 + Dim oFileVersion As Integer = 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 pFilePath.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 = CInt(Math.Floor(oFileNameWithoutExtension.Length / 2.0)) + Dim oNewFileNameWithoutExtension = oFileNameWithoutExtension.Substring(0, oNewLength) + _Logger.Info("New Filename will be: {0}", oNewFileNameWithoutExtension) + + oFileNameWithoutExtension = oNewFileNameWithoutExtension + End If + + ' while file exists, increment version. + ' version cannot go above MAX_FILE_VERSION, to prevent infinite loop + Do + oFinalFileName = Path.Combine(oDestinationDir, GetFilenameWithVersion(oFileNameWithoutExtension, oFileVersion, oExtension)) + _Logger.Debug("Intermediate Filename is {0}", oFinalFileName) + _Logger.Debug("File version: {0}", oFileVersion) + oFileVersion += 1 + Loop While pFileExistsAction(oFinalFileName) = True And oFileVersion < MAX_FILE_VERSION + + If oFileVersion >= MAX_FILE_VERSION Then + Throw New OverflowException($"Tried '{MAX_FILE_VERSION}' times to version filename before giving up. Sorry.") + End If + + _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!", pFilePath) + _Logger.Error(ex) + Return pFilePath + 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) As Tuple(Of String, Integer) + Dim oSplitString = pString.Split(VERSION_SEPARATOR).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 + + _Logger.Debug("Versioned: String [{0}], Version [{1}]", pString, oStringVersion) + + 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, FileVersion As Integer, Extension As String) As String + If FileVersion <= 1 Then + Return $"{FileNameWithoutExtension}{Extension}" + Else + Return $"{FileNameWithoutExtension}{VERSION_SEPARATOR}{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 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 + + ''' + ''' Checks the size of the supplied file. + ''' + ''' + ''' + ''' + Public Function TestFileSizeIsLessThanMaxFileSize(pFilePath As String, pMaxFileSizeInMegabytes As Integer) As Boolean + Dim oFileInfo As New FileInfo(pFilePath) + + _Logger.Info("Checking Filesize of {0}", oFileInfo.Name) + _Logger.Debug("Filesize threshold is {0} MB.", pMaxFileSizeInMegabytes) + + If pMaxFileSizeInMegabytes <= 0 Then + _Logger.Debug("Filesize is not configured. Skipping check.") + Return True + End If + + Dim oMaxSize = pMaxFileSizeInMegabytes * 1024 * 1024 + + If oMaxSize > 0 And oFileInfo.Length > oMaxSize Then + _Logger.Debug("Filesize is bigger than threshold.") + Return False + Else + _Logger.Debug("Filesize is smaller than threshold. All fine.") + Return True + End If + 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