Imports System.Data.SqlClient Imports DigitalData.Modules.Base.IDB.Constants Imports DigitalData.Modules.Database Imports DigitalData.Modules.Database.MSSQLServer.TransactionMode Imports DigitalData.Modules.Logging Imports DigitalData.Services.EDMIService.Security Namespace Methods.IDB.NewFile Public Class NewFileMethod Inherits BaseMethod Private ReadOnly Connection As SqlConnection Private ReadOnly Transaction As SqlTransaction Public Sub New(pLogConfig As LogConfig, pDatabaseIDB As MSSQLServer, pDatabaseECM As MSSQLServer, pGlobalState As GlobalState) MyBase.New(pLogConfig, pDatabaseIDB, pDatabaseECM, pGlobalState) Connection = DatabaseIDB.GetConnection() Transaction = Connection.BeginTransaction() End Sub Public Function Run(pData As NewFileRequest) As NewFileResponse Logger.Debug("Running [NewFileMethod].") Dim oFilePath As String = Nothing Try If pData.File Is Nothing Then Throw New ArgumentNullException(NameOf(pData.File)) End If If pData.KindType Is Nothing Then Throw New ArgumentNullException(NameOf(pData.KindType)) End If If pData.StoreName Is Nothing Then Throw New ArgumentNullException(NameOf(pData.StoreName)) End If If pData.User Is Nothing Then Throw New ArgumentNullException(NameOf(pData.User)) End If If IsNothing(pData.IDBDoctypeId) Then Throw New ArgumentNullException(NameOf(pData.IDBDoctypeId)) End If Logger.Debug("Checking if checksum already exists..") Dim oExistingObjectId = Helpers.TestFileChecksumExists(pData.File.FileChecksum) If oExistingObjectId > 0 Then Return New NewFileResponse(oExistingObjectId) End If Logger.Debug("Creating New ObjectId..") Dim oObjectId = Helpers.NewObjectIdWithTransaction(pData.KindType, pData.User.UserName, Connection, Transaction) If oObjectId = 0 Then LogAndThrow("Could not create new ObjectId!") End If Logger.Debug("New ObjectId [{0}] created!", oObjectId) ' Find ObjectStore by Title Logger.Debug("Checking for DataStore [{0}].", pData.StoreName) Dim oStore = GlobalState.ObjectStores. Where(Function(store) store.Title = pData.StoreName). SingleOrDefault() If oStore Is Nothing Then LogAndThrow($"DataStore [{pData.StoreName}] does not exist. Exiting.") End If Logger.Debug("Using DataStore [{0}].", pData.StoreName) ' Get Store base and final path Logger.Debug("Store BasePath is [{0}]", oStore.Path) Dim oFinalPath = Helpers.GetFileObjectPath(oStore, pData.File.FileImportedAt) ' Ensure target directory exists Try If Not IO.Directory.Exists(oFinalPath) Then IO.Directory.CreateDirectory(oFinalPath) End If Catch exDir As Exception LogAndThrow(exDir, $"Target directory [{oFinalPath}] could not be created.") End Try ' Get filename Dim oKeepFileName As Boolean = False If oStore.IsArchive Then Logger.Debug("Object Store is an archive: [{0}]", oStore.IsArchive) oKeepFileName = True End If Dim oFileName As String = GetFileObjectFileName(oObjectId, pData.File.FileName, oKeepFileName) Logger.Debug("Filename is [{0}]", oFileName) oFilePath = IO.Path.Combine(oFinalPath, oFileName) Dim oFileObjectInfo As IO.FileInfo = New IO.FileInfo(oFilePath) Dim oFileObjectName As String = oFileObjectInfo.Name Logger.Debug("File Information for [{0}]:", oFileObjectName) Dim oFileObjectSize As Long = pData.File.FileContents.Length ' original (plaintext) size Logger.Debug("Original Size: [{0}]", oFileObjectSize) Dim oOriginalExtension As String = pData.File.FileInfoRaw.Extension.Substring(1) Logger.Debug("Original Extension: [{0}]", oOriginalExtension) Logger.Debug("Checksum: [{0}]", pData.File.FileChecksum) ' Retrieve encryption password (environment variable) Dim encryptionPassword As String = Environment.GetEnvironmentVariable("DD_FILE_ENCRYPTION_PASSWORD") If String.IsNullOrWhiteSpace(encryptionPassword) Then LogAndThrow("Encryption password not configured (env DD_FILE_ENCRYPTION_PASSWORD).") End If ' Perform encryption with strict failure handling Try Logger.Info("Encrypting and saving file to path [{0}]", oFilePath) SecureFileHandler.EncryptFileFromBytes(pData.File.FileContents, oFilePath, encryptionPassword) Catch exEnc As Exception LogAndThrow(exEnc, $"Could not encrypt/write file [{oFilePath}] to disk!") End Try ' Post-encryption validation: file must exist and contain at least header bytes Try Dim fi As New IO.FileInfo(oFilePath) If Not fi.Exists Then LogAndThrow($"Encrypted file was not created at [{oFilePath}].") End If ' Minimum file size:1 (version) +4 (iterations) +32 (salt) =37 bytes If fi.Length < 37 Then LogAndThrow($"Encrypted file at [{oFilePath}] is invalid or truncated (size {fi.Length}).") End If Logger.Debug("Encrypted physical file size: [{0}]", fi.Length) Catch exVal As Exception ' LogAndThrow above will throw; any other IO errors should also abort here LogAndThrow(exVal, "Encrypted file validation failed.") End Try '--------------------------------------------------------------------------- Logger.Info("Creating IDB FileObject for ObjectId [{0}].", oObjectId) ' Insert into DB (store original plaintext size for consistency) Dim oSQL As String = $"EXEC PRIDB_NEW_IDBFO '{oFinalPath}', '{oFileObjectName}', '{oOriginalExtension}', {oFileObjectSize}, '{pData.File.FileChecksum}' , '{pData.User.UserName}', '{oObjectId}', {oStore.Id}, {pData.IDBDoctypeId}" Dim oResult As Boolean = DatabaseIDB.ExecuteNonQueryWithConnectionObject(oSQL, Connection, ExternalTransaction, Transaction) If oResult = False Then LogAndThrow("IDB FileObject could not be created!") End If '--------------------------------------------------------------------------- Dim oSystemAttributes As New Dictionary(Of String, Object) From { {Attributes.ATTRIBUTE_ORIGIN_FILENAME, pData.File.FileName}, {Attributes.ATTRIBUTE_ORIGIN_CREATED, pData.File.FileCreatedAt}, {Attributes.ATTRIBUTE_ORIGIN_CHANGED, pData.File.FileChangedAt} } For Each oAttribute As KeyValuePair(Of String, Object) In oSystemAttributes Try ' Dont write empty attributes If oAttribute.Value Is Nothing Then Continue For End If Dim oSuccess = Helpers.SetAttributeValueWithTransaction(Connection, Transaction, oObjectId, oAttribute.Key, oAttribute.Value, pData.User.Language, pData.User.UserName) If oSuccess Then Logger.Debug("System Attribute [{0}] written with value [{1}]", oAttribute.Key, oAttribute.Value) Else Logger.Warn("System attribute value could not be written") End If Catch ex As Exception LogAndThrow(ex, $"System attribute [{oAttribute.Key}] could not be written!") End Try Next '--------------------------------------------------------------------------- ' Finally, commit the transaction Transaction?.Commit() Return New NewFileResponse(oObjectId) Catch ex As Exception Logger.Warn("Error occurred while creating file!") Logger.Error(ex) Logger.Info("Cleaning up files.") If Not IsNothing(oFilePath) AndAlso IO.File.Exists(oFilePath) Then Try IO.File.Delete(oFilePath) Catch exInner As Exception Logger.Warn("Error while cleaning up files.") Logger.Error(exInner) End Try End If Logger.Info("Rolling back transaction.") Transaction?.Rollback() Return New NewFileResponse(ex) End Try End Function Public Function Run_Old(pData As NewFileRequest) As NewFileResponse Logger.Debug("Running [NewFileMethod Old].") Dim oFilePath As String = Nothing Try If pData.File Is Nothing Then Throw New ArgumentNullException(NameOf(pData.File)) End If If pData.KindType Is Nothing Then Throw New ArgumentNullException(NameOf(pData.KindType)) End If If pData.StoreName Is Nothing Then Throw New ArgumentNullException(NameOf(pData.StoreName)) End If If pData.User Is Nothing Then Throw New ArgumentNullException(NameOf(pData.User)) End If If IsNothing(pData.IDBDoctypeId) Then Throw New ArgumentNullException(NameOf(pData.IDBDoctypeId)) End If Logger.Debug("Checking if checksum already exists..") Dim oExistingObjectId = Helpers.TestFileChecksumExists(pData.File.FileChecksum) If oExistingObjectId > 0 Then Return New NewFileResponse(oExistingObjectId) End If Logger.Debug("Creating New ObjectId..") Dim oObjectId = Helpers.NewObjectIdWithTransaction(pData.KindType, pData.User.UserName, Connection, Transaction) If oObjectId = 0 Then LogAndThrow("Could not create new ObjectId!") End If Logger.Debug("New ObjectId [{0}] created!", oObjectId) ' Find ObjectStore by Title Logger.Debug("Checking for DataStore [{0}].", pData.StoreName) Dim oStore = GlobalState.ObjectStores. Where(Function(store) store.Title = pData.StoreName). SingleOrDefault() If oStore Is Nothing Then LogAndThrow($"DataStore [{pData.StoreName}] does not exist. Exiting.") End If Logger.Debug("Using DataStore [{0}].", pData.StoreName) ' Get Store base and final path Logger.Debug("Store BasePath is [{0}]", oStore.Path) Dim oFinalPath = Helpers.GetFileObjectPath(oStore, pData.File.FileImportedAt) ' Get filename Dim oKeepFileName As Boolean = False If oStore.IsArchive Then Logger.Debug("Object Store is an archive: [{0}]", oStore.IsArchive) oKeepFileName = True End If Dim oFileName As String = GetFileObjectFileName(oObjectId, pData.File.FileName, oKeepFileName) Logger.Debug("Filename is [{0}]", oFileName) oFilePath = IO.Path.Combine(oFinalPath, oFileName) Dim oFileObjectInfo As IO.FileInfo = New IO.FileInfo(oFilePath) Dim oFileObjectName As String = oFileObjectInfo.Name Logger.Debug("File Information for [{0}]:", oFileObjectName) Dim oFileObjectSize As Long = pData.File.FileContents.Length Logger.Debug("Size: [{0}]", oFileObjectSize) Dim oOriginalExtension As String = pData.File.FileInfoRaw.Extension.Substring(1) Logger.Debug("Original Extension: [{0}]", oOriginalExtension) Logger.Debug("Checksum: [{0}]", pData.File.FileChecksum) Try Using oStream = New IO.FileStream(oFilePath, IO.FileMode.Create, IO.FileAccess.Write) Logger.Info("Saving file to path [{0}]", oFilePath) oStream.Write(pData.File.FileContents, 0, oFileObjectSize) oStream.Flush(True) oStream.Close() End Using Catch ex As Exception LogAndThrow(ex, $"Could not write file [{oFilePath}] to disk!") End Try '--------------------------------------------------------------------------- Logger.Info("Creating IDB FileObject for ObjectId [{0}].", oObjectId) ' Insert into DB Dim oSQL As String = $"EXEC PRIDB_NEW_IDBFO '{oFinalPath}', '{oFileObjectName}', '{oOriginalExtension}', {oFileObjectSize}, '{pData.File.FileChecksum}' , '{pData.User.UserName}', '{oObjectId}', {oStore.Id}, {pData.IDBDoctypeId}" Dim oResult As Boolean = DatabaseIDB.ExecuteNonQueryWithConnectionObject(oSQL, Connection, ExternalTransaction, Transaction) If oResult = False Then LogAndThrow("IDB FileObject could not be created!") End If '--------------------------------------------------------------------------- Dim oSystemAttributes As New Dictionary(Of String, Object) From { {Attributes.ATTRIBUTE_ORIGIN_FILENAME, pData.File.FileName}, {Attributes.ATTRIBUTE_ORIGIN_CREATED, pData.File.FileCreatedAt}, {Attributes.ATTRIBUTE_ORIGIN_CHANGED, pData.File.FileChangedAt} } For Each oAttribute As KeyValuePair(Of String, Object) In oSystemAttributes Try ' Dont write empty attributes If oAttribute.Value Is Nothing Then Continue For End If Dim oSuccess = Helpers.SetAttributeValueWithTransaction(Connection, Transaction, oObjectId, oAttribute.Key, oAttribute.Value, pData.User.Language, pData.User.UserName) If oSuccess Then Logger.Debug("System Attribute [{0}] written with value [{1}]", oAttribute.Key, oAttribute.Value) Else Logger.Warn("System attribute value could not be written") End If Catch ex As Exception LogAndThrow(ex, $"System attribute [{oAttribute.Key}] could not be written!") End Try Next '--------------------------------------------------------------------------- ' Finally, commit the transaction Transaction?.Commit() Return New NewFileResponse(oObjectId) Catch ex As Exception Logger.Warn("Error occurred while creating file!") Logger.Error(ex) Logger.Info("Cleaning up files.") If Not IsNothing(oFilePath) AndAlso IO.File.Exists(oFilePath) Then Try IO.File.Delete(oFilePath) Catch exInner As Exception Logger.Warn("Error while cleaning up files.") Logger.Error(exInner) End Try End If Logger.Info("Rolling back transaction.") Transaction?.Rollback() Return New NewFileResponse(ex) End Try End Function Private Function GetFileObjectFileName(IDB_OBJ_ID As Long, pFilename As String, pKeepFilename As Boolean) As String ' TODO: save actual extensions If pKeepFilename Then Return pFilename Else Return $"{IDB_OBJ_ID}.ddfo" End If End Function End Class End Namespace