/* * Original work: Copyright (c) 2014, Oculus VR, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * RakNet License.txt file in the licenses directory of this source tree. An additional grant * of patent rights can be found in the RakNet Patents.txt file in the same directory. * * * Modified work: Copyright (c) 2016-2021, SLikeSoft UG (haftungsbeschränkt) * * This source code was modified by SLikeSoft. Modifications are licensed under the MIT-style * license found in the license.txt file in the root directory of this source tree. */ #include "slikenet/string.h" #include "AutopatcherMySQLRepository.h" #include "slikenet/AutopatcherPatchContext.h" #include "slikenet/FileList.h" #include "slikenet/assert.h" #include "slikenet/DS_List.h" // ntohl #ifdef _WIN32 #include #else #include #endif // If you get fatal error C1083: Cannot open include file: 'mysql.h' then you need to install MySQL. See readme.txt in this sample directory. #include "mysql.h" #include "CreatePatch.h" #include "slikenet/AutopatcherPatchContext.h" // #include "slikenet/DR_SHA1.h" #include #include "slikenet/LinuxStrings.h" #include "slikenet/linux_adapter.h" #include "slikenet/osx_adapter.h" using namespace SLNet; static const unsigned HASH_LENGTH = sizeof(unsigned int); struct FileInfo { SLNet::RakString filename; char contentHash[HASH_LENGTH]; bool createFile; }; // alloca #ifdef _COMPATIBILITY_1 #elif defined(_WIN32) #include #else //#include #endif #define PQEXECPARAM_FORMAT_TEXT 0 #define PQEXECPARAM_FORMAT_BINARY 1 AutopatcherMySQLRepository::AutopatcherMySQLRepository() { filePartConnection = nullptr; } AutopatcherMySQLRepository::~AutopatcherMySQLRepository() { if (filePartConnection) mysql_close(filePartConnection); } bool AutopatcherMySQLRepository::CreateAutopatcherTables(void) { if (!IsConnected()) return false; //sqlCommandMutex.Lock(); ExecuteBlockingCommand("BEGIN;"); if (!ExecuteBlockingCommand( "CREATE TABLE Applications (" "applicationID INT AUTO_INCREMENT," "applicationName VARCHAR(255) NOT NULL UNIQUE," "changeSetID integer NOT NULL DEFAULT 0," "userName TEXT NOT NULL," "PRIMARY KEY (applicationID));" )) {Rollback(); return false;} if (!ExecuteBlockingCommand( "CREATE TABLE FileVersionHistory (" "fileID INT AUTO_INCREMENT," "applicationID INT NOT NULL, " "filename VARCHAR(255) NOT NULL," "fileLength INT," "content LONGBLOB," "contentHash TINYBLOB," "patch LONGBLOB," "createFile TINYINT NOT NULL," "modificationDate double precision," "lastSentDate double precision," "timesSent INT NOT NULL DEFAULT 0," "changeSetID INT NOT NULL," "userName TEXT NOT NULL," "PRIMARY KEY (fileID)); " )) {Rollback(); return false;} if (!ExecuteBlockingCommand( "CREATE INDEX FV_appID on FileVersionHistory(applicationID);" )) {Rollback(); return false;} if (!ExecuteBlockingCommand( "CREATE INDEX FV_fname on FileVersionHistory(filename);" )) {Rollback(); return false;} if (!ExecuteBlockingCommand( "CREATE VIEW AutoPatcherView AS SELECT " "FileVersionHistory.applicationid," "Applications.applicationName," "FileVersionHistory.fileID," "FileVersionHistory.fileName," "FileVersionHistory.createFile," "FileVersionHistory.fileLength," "FileVersionHistory.changeSetID," "FileVersionHistory.lastSentDate," "FileVersionHistory.modificationDate," "FileVersionHistory.timesSent " "FROM (FileVersionHistory JOIN Applications ON " "( FileVersionHistory.applicationID = Applications.applicationID )) " "ORDER BY Applications.applicationID ASC, FileVersionHistory.fileID ASC;" )) {Rollback(); return false;} bool b = ExecuteBlockingCommand("COMMIT;"); //sqlCommandMutex.Unlock(); return b; } bool AutopatcherMySQLRepository::DestroyAutopatcherTables(void) { if (!IsConnected()) return false; //sqlCommandMutex.Lock(); ExecuteBlockingCommand("DROP INDEX FV_appID;"); ExecuteBlockingCommand("DROP INDEX FV_fname;"); ExecuteBlockingCommand("DROP TABLE Applications CASCADE;"); ExecuteBlockingCommand("DROP TABLE FileVersionHistory CASCADE;"); bool b = ExecuteBlockingCommand("DROP VIEW AutoPatcherView;"); //sqlCommandMutex.Unlock(); return b; } bool AutopatcherMySQLRepository::AddApplication(const char *applicationName, const char *userName) { // mysql_real_escape_string char query[512]; sprintf_s(query, "INSERT INTO Applications (applicationName, userName) VALUES ('%s', '%s');", GetEscapedString(applicationName).C_String(), GetEscapedString(userName).C_String()); //sqlCommandMutex.Lock(); bool b = ExecuteBlockingCommand(query); //sqlCommandMutex.Unlock(); return b; } bool AutopatcherMySQLRepository::RemoveApplication(const char *applicationName) { char query[512]; sprintf_s(query, "DELETE FROM Applications WHERE applicationName='%s';", GetEscapedString(applicationName).C_String()); //sqlCommandMutex.Lock(); bool b = ExecuteBlockingCommand(query); //sqlCommandMutex.Unlock(); return b; } bool AutopatcherMySQLRepository::GetChangelistSinceDate(const char *applicationName, FileList *addedOrModifiedFilesWithHashData, FileList *deletedFiles, double sinceDate) { char query[512]; SLNet::RakString escapedApplicationName = GetEscapedString(applicationName); sprintf_s(query, "SELECT applicationID FROM Applications WHERE applicationName='%s';", escapedApplicationName.C_String()); int applicationID; //sqlCommandMutex.Lock(); if (!ExecuteQueryReadInt(query, &applicationID)) { // This message covers the lost connection to the SQL server //sqlCommandMutex.Unlock(); //sprintf_s(lastError,"ERROR: %s not found in UpdateApplicationFiles\n",escapedApplicationName.C_String()); return false; } //sqlCommandMutex.Unlock(); if (sinceDate!=0) sprintf_s(query, "SELECT filename, fileLength, contentHash, createFile, fileId FROM FileVersionHistory " "JOIN (SELECT max(fileId) maxId FROM FileVersionHistory WHERE applicationId=%i AND modificationDate > %f GROUP BY fileName) MaxId " "ON FileVersionHistory.fileId = MaxId.maxId " "ORDER BY filename DESC;", applicationID,sinceDate); else sprintf_s(query, "SELECT filename, fileLength, contentHash, createFile, fileId FROM FileVersionHistory " "JOIN (SELECT max(fileId) maxId FROM FileVersionHistory WHERE applicationId=%i GROUP BY fileName) MaxId " "ON FileVersionHistory.fileId = MaxId.maxId " "ORDER BY filename DESC;", applicationID); MYSQL_RES *result = nullptr; //sqlCommandMutex.Lock(); if (!ExecuteBlockingCommand(query, &result)) { //sqlCommandMutex.Unlock(); return false; } //sqlCommandMutex.Unlock(); MYSQL_ROW row; while ((row = mysql_fetch_row(result)) != nullptr) { const char * createFileResult = row[3]; const char * hardDriveFilename = row[0]; if (createFileResult[0] == '1') { const char *hardDriveHash = row[2]; int fileLength = atoi(row[1]); addedOrModifiedFilesWithHashData->AddFile(hardDriveFilename, hardDriveFilename, hardDriveHash, HASH_LENGTH, fileLength, FileListNodeContext(), false); } else { deletedFiles->AddFile(hardDriveFilename, hardDriveFilename, nullptr, 0, 0, FileListNodeContext(), false); } } mysql_free_result(result); return true; } int AutopatcherMySQLRepository::GetPatches(const char *applicationName, FileList *input, bool allowDownloadOfOriginalUnmodifiedFiles, FileList *patchList) { char query[512]; SLNet::RakString escapedApplicationName = GetEscapedString(applicationName); sprintf_s(query, "SELECT applicationID FROM Applications WHERE applicationName='%s';", escapedApplicationName.C_String()); int applicationID; //sqlCommandMutex.Lock(); if (!ExecuteQueryReadInt(query, &applicationID)) { //sqlCommandMutex.Unlock(); sprintf_s(lastError,"ERROR: %s not found in GetPatches\n",applicationName); return false; } //sqlCommandMutex.Unlock(); // Go through the input list. for (unsigned inputIndex=0; inputIndex < input->fileList.Size(); inputIndex++) { const char * userHash=input->fileList[inputIndex].data; const char * userFilename=input->fileList[inputIndex].filename; char *fn = new char[(strlen(userFilename))*2+1]; mysql_real_escape_string(mySqlConnection, fn, userFilename, (unsigned long)strlen(userFilename)); if (userHash == nullptr) { // If the user does not have a hash in the input list, get the contents of latest version of this named file and write it to the patch list // sprintf_s(query, "SELECT content FROM FileVersionHistory " // "JOIN (SELECT max(fileId) maxId FROM FileVersionHistory WHERE applicationId=%i AND filename='%s') MaxId " // "ON FileVersionHistory.fileId = MaxId.maxId", // applicationID, fn); sprintf_s(query, "SELECT fileId, fileLength, changeSetID FROM FileVersionHistory " "JOIN (SELECT max(fileId) maxId FROM FileVersionHistory WHERE applicationId=%i AND filename='%s') MaxId " "ON FileVersionHistory.fileId = MaxId.maxId", applicationID, fn); MYSQL_RES *result = nullptr; //sqlCommandMutex.Lock(); if (!ExecuteBlockingCommand(query, &result)) { //sqlCommandMutex.Unlock(); delete[] fn; return false; } //sqlCommandMutex.Unlock(); MYSQL_ROW row = mysql_fetch_row(result); if (row != nullptr) { //const char * content = row [0]; //unsigned long contentLength=mysql_fetch_lengths (result) [0]; //patchList->AddFile(userFilename, content, contentLength, contentLength, FileListNodeContext(PC_WRITE_FILE,0)); const int fileId = atoi(row[0]); const int fileLength = atoi(row[1]); const int changeSetID = atoi(row[2]); if (!allowDownloadOfOriginalUnmodifiedFiles && changeSetID==0) { printf("Failure: allowDownloadOfOriginalUnmodifiedFiles is false for %s length %i\n", userFilename, fileLength); mysql_free_result(result); return false; } patchList->AddFile(userFilename,userFilename, nullptr, fileLength, fileLength, FileListNodeContext(PC_WRITE_FILE, fileId, 0, 0), true); } mysql_free_result(result); } else // Assuming the user does have a hash. { if (input->fileList[inputIndex].dataLengthBytes!=HASH_LENGTH) { delete[] fn; return false; } // Get the hash and ID of the latest version of this file, by filename. sprintf_s(query, "SELECT contentHash, fileId, fileLength FROM FileVersionHistory " "JOIN (SELECT max(fileId) maxId FROM FileVersionHistory WHERE applicationId=%i AND filename='%s') MaxId " "ON FileVersionHistory.fileId = MaxId.maxId", applicationID, fn); MYSQL_RES *result = nullptr; //sqlCommandMutex.Lock(); if (!ExecuteBlockingCommand (query, &result)) { //sqlCommandMutex.Unlock(); delete [] fn; return false; } //sqlCommandMutex.Unlock(); MYSQL_ROW row = mysql_fetch_row(result); if (row != nullptr) { const char * contentHash = row[0]; const int fileId = atoi(row[1]); const int fileLength = atoi(row[2]); // double check if this works if (memcmp(contentHash, userHash, HASH_LENGTH)!=0) { char buf[2 * HASH_LENGTH + 1]; mysql_real_escape_string(mySqlConnection, buf, userHash, HASH_LENGTH); sprintf_s(query, "SELECT patch FROM FileVersionHistory WHERE applicationId=%i AND filename='%s' AND contentHash='%s'; ", applicationID, fn, buf); MYSQL_RES * patchResult = nullptr; //sqlCommandMutex.Lock(); if (!ExecuteBlockingCommand(query, &patchResult)) { //sqlCommandMutex.Unlock(); delete [] fn; return false; } //sqlCommandMutex.Unlock(); row = mysql_fetch_row(patchResult); if (row == nullptr) { // If no patch found, then this is a non-release version, or a very old version we are no longer tracking. // Get the contents of latest version of this named file by fileId and return it. /* sprintf_s(query, "SELECT content FROM FileVersionHistory WHERE fileId=%d;", fileId); if (mysql_query(mySqlConnection, query) != 0) { delete[] fn; strcpy(lastError, mysql_error(mySqlConnection)); return false; } MYSQL_RES *substrresult = mysql_store_result(mySqlConnection); row = mysql_fetch_row(substrresult); char *file = row[0]; unsigned long contentLength = mysql_fetch_lengths(substrresult)[0]; patchList->AddFile(userFilename, file, fileLength, contentLength, FileListNodeContext(PC_WRITE_FILE, 0)); mysql_free_result(substrresult); */ patchList->AddFile(userFilename, userFilename, nullptr, fileLength, fileLength, FileListNodeContext(PC_WRITE_FILE, fileId, 0, 0), true); } else { // Otherwise, write the hash of the new version and then write the patch to get to that version. // char *patch = row[0]; unsigned long patchLength = mysql_fetch_lengths(patchResult)[0]; char *temp = new char[patchLength + HASH_LENGTH]; memcpy(temp, contentHash, HASH_LENGTH); memcpy(temp+HASH_LENGTH, patch, patchLength); patchList->AddFile(userFilename,userFilename, temp, HASH_LENGTH+patchLength, fileLength, FileListNodeContext(PC_HASH_1_WITH_PATCH, 0, 0, 0)); delete[] temp; } mysql_free_result(patchResult); } else { // else if the hash of this file matches what the user has, the user has the latest version. Done. } } else { // else if there is no such file, skip this file. } mysql_free_result(result); } delete[] fn; } return true; } bool AutopatcherMySQLRepository::GetMostRecentChangelistWithPatches(SLNet::RakString &applicationName, FileList *patchedFiles, FileList *addedFiles, FileList *addedOrModifiedFileHashes, FileList *deletedFiles, double *priorRowPatchTime, double *mostRecentRowPatchTime) { // unused parameters (void)applicationName; (void)patchedFiles; (void)addedFiles; (void)addedOrModifiedFileHashes; (void)deletedFiles; (void)priorRowPatchTime; (void)mostRecentRowPatchTime; // Not yet implemented return false; } bool AutopatcherMySQLRepository::UpdateApplicationFiles(const char *applicationName, const char *applicationDirectory, const char *userName, FileListProgress *cb) { MYSQL_STMT *stmt; MYSQL_BIND bind[3]; char query[512]; FileList filesOnHarddrive; filesOnHarddrive.AddCallback(cb); int prepareResult; my_bool falseVar = false; SLNet::RakString escapedApplicationName = GetEscapedString(applicationName); filesOnHarddrive.AddFilesFromDirectory(applicationDirectory,"", true, true, true, FileListNodeContext()); if (filesOnHarddrive.fileList.Size() == 0) { sprintf_s(lastError,"ERROR: Can't find files at %s in UpdateApplicationFiles\n",applicationDirectory); return false; } sprintf_s(query, "SELECT applicationID FROM Applications WHERE applicationName='%s';", escapedApplicationName.C_String()); int applicationID; //sqlCommandMutex.Lock(); if (!ExecuteQueryReadInt(query, &applicationID)) { //sqlCommandMutex.Unlock(); sprintf_s(lastError,"ERROR: %s not found in UpdateApplicationFiles\n",escapedApplicationName.C_String()); return false; } if (!ExecuteBlockingCommand("BEGIN;")) { //sqlCommandMutex.Unlock(); return false; } //sqlCommandMutex.Unlock(); sprintf_s(query, "UPDATE Applications SET changeSetId = changeSetId + 1 where applicationID=%i;", applicationID); //sqlCommandMutex.Lock(); if (!ExecuteBlockingCommand(query)) { Rollback (); //sqlCommandMutex.Unlock(); return false; } //sqlCommandMutex.Unlock(); int changeSetId = 0; sprintf_s(query, "SELECT changeSetId FROM Applications WHERE applicationID=%i;", applicationID); //sqlCommandMutex.Lock(); if (!ExecuteQueryReadInt(query, &changeSetId)) { Rollback (); //sqlCommandMutex.Unlock(); return false; } //sqlCommandMutex.Unlock(); // +1 was added in the update changeSetId--; // Gets all newest files sprintf_s(query, "SELECT filename, contentHash, createFile FROM FileVersionHistory " "JOIN (SELECT max(fileId) maxId FROM FileVersionHistory WHERE applicationId=%i GROUP BY fileName) MaxId " "ON FileVersionHistory.fileId = MaxId.maxId " "ORDER BY filename DESC;", applicationID); MYSQL_RES *result = nullptr; //sqlCommandMutex.Lock(); if (!ExecuteBlockingCommand(query, &result)) { Rollback(); //sqlCommandMutex.Unlock(); return false; } //sqlCommandMutex.Unlock(); DataStructures::List newestFiles; MYSQL_ROW row; while ((row = mysql_fetch_row(result)) != nullptr) { FileInfo fi; fi.filename = row[0]; fi.createFile = (atoi(row[2]) != 0); if (fi.createFile) { RakAssert(mysql_fetch_lengths(result)[1] == HASH_LENGTH); // check the data is sensible memcpy(fi.contentHash, row[1], HASH_LENGTH); } newestFiles.Insert(fi, _FILE_AND_LINE_ ); } mysql_free_result(result); FileList newFiles; // Loop through files on filesOnHarddrive // If the file in filesOnHarddrive does not exist in the query result, or if it does but the hash is different or non-existent, add this file to the create list for (unsigned fileListIndex = 0; fileListIndex < filesOnHarddrive.fileList.Size(); fileListIndex++) { bool addFile=true; if (fileListIndex%10 == 0) printf("Hashing files %i/%i\n", fileListIndex+1, filesOnHarddrive.fileList.Size()); const char * hardDriveFilename=filesOnHarddrive.fileList[fileListIndex].filename; const char * hardDriveHash=filesOnHarddrive.fileList[fileListIndex].data; for (unsigned i = 0; i != newestFiles.Size (); ++i) { const FileInfo & fi = newestFiles [i]; if (_stricmp(hardDriveFilename, fi.filename) == 0) { if (fi.createFile && memcmp(fi.contentHash, hardDriveHash, HASH_LENGTH)==0) { // File exists in database and is the same addFile=false; } break; } } // Unless set to false, file does not exist in query result or is different. if (addFile) { newFiles.AddFile(hardDriveFilename,hardDriveFilename, filesOnHarddrive.fileList[fileListIndex].data, filesOnHarddrive.fileList[fileListIndex].dataLengthBytes, filesOnHarddrive.fileList[fileListIndex].fileLengthBytes, FileListNodeContext(), false, true); filesOnHarddrive.fileList[fileListIndex].data = nullptr; } } // Go through query results that are marked as create // If a file that is currently in the database is not on the harddrive, add it to the delete list FileList deletedFiles; for (unsigned i = 0; i != newestFiles.Size (); ++i) { const FileInfo & fi = newestFiles [i]; if (!fi.createFile) continue; // If already false don't mark false again. bool fileOnHarddrive = false; for (unsigned fileListIndex = 0; fileListIndex < filesOnHarddrive.fileList.Size(); fileListIndex++) { const char *hardDriveFilename = filesOnHarddrive.fileList[fileListIndex].filename; //hardDriveHash = filesOnHarddrive.fileList[fileListIndex].data; if (_stricmp(hardDriveFilename, fi.filename) == 0) { fileOnHarddrive=true; break; } } if (!fileOnHarddrive) deletedFiles.AddFile(fi.filename, fi.filename, nullptr, 0, 0, FileListNodeContext(), false); } // files on harddrive no longer needed. Free this memory since generating all the patches is memory intensive. filesOnHarddrive.Clear(); // For each file in the delete list add a row indicating file deletion for (unsigned fileListIndex = 0; fileListIndex < deletedFiles.fileList.Size(); fileListIndex++) { if (fileListIndex%10 == 0) printf("Tagging deleted files %i/%i\n", fileListIndex+1, deletedFiles.fileList.Size()); sprintf_s(query, "INSERT INTO FileVersionHistory(applicationID, filename, createFile, changeSetID, userName, modificationDate) VALUES (%i, '%s', FALSE,%i,'%s',%lld);", applicationID, GetEscapedString(deletedFiles.fileList[fileListIndex].filename).C_String(), changeSetId, GetEscapedString(userName).C_String(), time(nullptr)); //sqlCommandMutex.Lock(); if (!ExecuteBlockingCommand (query)) { Rollback(); //sqlCommandMutex.Unlock(); deletedFiles.Clear(); newFiles.Clear(); return false; } //sqlCommandMutex.Unlock(); } // Clear the delete list as it is no longer needed. deletedFiles.Clear(); // For each file in the create list for (unsigned fileListIndex = 0; fileListIndex < newFiles.fileList.Size(); fileListIndex++) { if (fileListIndex%10 == 0) printf("Adding file %i/%i\n", fileListIndex+1, newFiles.fileList.Size()); const char * hardDriveFilename=newFiles.fileList[fileListIndex].filename; const char * hardDriveData=newFiles.fileList[fileListIndex].data+HASH_LENGTH; const char * hardDriveHash=newFiles.fileList[fileListIndex].data; unsigned hardDriveDataLength=newFiles.fileList[fileListIndex].fileLengthBytes; sprintf_s(query, "SELECT fileID from FileVersionHistory WHERE applicationID=%i AND filename='%s' AND createFile=TRUE;", applicationID, GetEscapedString(hardDriveFilename).C_String()); MYSQL_RES *res = nullptr; //sqlCommandMutex.Lock(); if (!ExecuteBlockingCommand(query, &res)) { Rollback(); //sqlCommandMutex.Unlock(); newFiles.Clear(); return false; } //sqlCommandMutex.Unlock(); // Create new patches for every create version while ((row = mysql_fetch_row(res)) != nullptr) { const char *fileID = row[0]; // The last query handled all the relevant comparisons sprintf_s(query, "SELECT content from FileVersionHistory WHERE fileID=%s", fileID); MYSQL_RES *queryResult = nullptr; //sqlCommandMutex.Lock(); if (!ExecuteBlockingCommand(query, &queryResult)) { Rollback(); //sqlCommandMutex.Unlock(); newFiles.Clear(); mysql_free_result(res); return false; } //sqlCommandMutex.Unlock(); MYSQL_ROW queryRow = mysql_fetch_row(queryResult); const unsigned contentLength = mysql_fetch_lengths(queryResult)[0]; const char *content = queryRow[0]; char *patch; unsigned patchLength; if (!CreatePatch(content, contentLength, (char*)hardDriveData, hardDriveDataLength, &patch, &patchLength)) { strcpy_s(lastError,"CreatePatch failed.\n"); Rollback(); newFiles.Clear(); mysql_free_result(res); mysql_free_result(queryResult); return false; } char buf[512]; stmt = mysql_stmt_init(mySqlConnection); sprintf(buf, "UPDATE FileVersionHistory SET patch=? where fileID=%s;", fileID); if ((prepareResult=mysql_stmt_prepare(stmt, buf, (unsigned long)strlen(buf))) != 0) { strcpy(lastError, mysql_stmt_error(stmt)); mysql_stmt_close(stmt); Rollback(); return false; } memset(bind, 0, sizeof(bind)); unsigned long l1; l1 = patchLength; bind[0].buffer_type = MYSQL_TYPE_LONG_BLOB; bind[0].buffer = patch; bind[0].buffer_length = patchLength; bind[0].is_null = &falseVar; bind[0].length = &l1; if (mysql_stmt_bind_param(stmt, bind)) { strcpy(lastError, mysql_stmt_error(stmt)); mysql_stmt_close(stmt); Rollback(); return false; } //sqlCommandMutex.Lock(); if (mysql_stmt_execute(stmt)) { strcpy(lastError, mysql_stmt_error(stmt)); mysql_stmt_close(stmt); Rollback(); //sqlCommandMutex.Unlock(); newFiles.Clear(); mysql_free_result(res); mysql_free_result(queryResult); delete[] patch; return false; } //sqlCommandMutex.Unlock(); mysql_stmt_close(stmt); delete[] patch; mysql_free_result(queryResult); } mysql_free_result(res); stmt = mysql_stmt_init(mySqlConnection); sprintf_s(query, "INSERT INTO FileVersionHistory (applicationID, filename, fileLength, content, contentHash, createFile, changeSetID, userName, modificationDate) " "VALUES (%i, ?, %i,?,?, TRUE, %i, '%s', %lld);", applicationID, hardDriveDataLength, changeSetId, GetEscapedString(userName).C_String(), time(nullptr)); if ((prepareResult = mysql_stmt_prepare(stmt, query, (unsigned long) strlen(query))) != 0) { strcpy(lastError, mysql_stmt_error (stmt)); mysql_stmt_close(stmt); Rollback(); return false; } memset(bind, 0, sizeof(bind)); unsigned long l2,l3,l4; l2 = (unsigned long)strlen(hardDriveFilename); l3 = hardDriveDataLength; l4 = HASH_LENGTH; bind[0].buffer_type = MYSQL_TYPE_STRING; bind[0].buffer = (void*)hardDriveFilename; bind[0].buffer_length = (unsigned long)strlen(hardDriveFilename); bind[0].is_null = &falseVar; bind[0].length = &l2; bind[1].buffer_type = MYSQL_TYPE_LONG_BLOB; bind[1].buffer = (void*) hardDriveData; bind[1].buffer_length = hardDriveDataLength; bind[1].is_null = &falseVar; bind[1].length = &l3; bind[2].buffer_type = MYSQL_TYPE_TINY_BLOB; bind[2].buffer = (void*)hardDriveHash; bind[2].buffer_length = HASH_LENGTH; bind[2].is_null = &falseVar; bind[2].length = &l4; if (mysql_stmt_bind_param(stmt, bind)) { strcpy(lastError, mysql_stmt_error(stmt)); mysql_stmt_close(stmt); Rollback(); return false; } //sqlCommandMutex.Lock(); if (mysql_stmt_execute(stmt)) { strcpy(lastError, mysql_stmt_error(stmt)); mysql_stmt_close(stmt); Rollback(); //sqlCommandMutex.Unlock(); return false; } //sqlCommandMutex.Unlock(); mysql_stmt_close(stmt); } //sqlCommandMutex.Lock(); if (!ExecuteBlockingCommand("COMMIT;")) { Rollback(); //sqlCommandMutex.Unlock(); return false; } //sqlCommandMutex.Unlock(); return true; } const char *AutopatcherMySQLRepository::GetLastError(void) const { return MySQLInterface::GetLastError(); } unsigned int AutopatcherMySQLRepository::GetFilePart(const char *filename, unsigned int startReadBytes, unsigned int numBytesToRead, void *preallocatedDestination, FileListNodeContext context) { // unused parameters (void)filename; char query[512]; sprintf_s(query, "SELECT substring(content from %i for %i) FROM FileVersionHistory WHERE fileId=%i;", startReadBytes+1, numBytesToRead, context.flnc_extraData1); // CREATE NEW CONNECTION JUST FOR THIS QUERY // This is because the autopatcher is sharing this class, but this is called from multiple threads and mysql is not threadsafe MYSQL_RES *result; char error[512]; filePartConnectionMutex.Lock(); if (filePartConnection == nullptr) { filePartConnection = mysql_init(nullptr); // #high - missing nullptr handling // #med - revise usage of reconnection handling - maybe better to explicitly disable? - revise handling in PostGreRepo // #med - consider using MySQLInterface class for the connection my_bool reconnect = 1; if (mysql_options(filePartConnection, MYSQL_OPT_RECONNECT, &reconnect) != 0) { mysql_close(filePartConnection); filePartConnection = nullptr; // #high - error reporting filePartConnectionMutex.Unlock(); return 0; } // #high - missing error handling upon connection failure mysql_real_connect(filePartConnection, _host, _user, _passwd, _db, _port, _unix_socket, _clientflag); } if (mysql_query(filePartConnection, query) != 0) { // #med - review --- should this set the class member maybe? strcpy(error, mysql_error(filePartConnection)); } result = mysql_store_result(filePartConnection); if (result) { // This has very poor performance with any size for GetIncrementalReadChunkSize, but especially for larger sizes MYSQL_ROW row = mysql_fetch_row(result); if (row != nullptr) { const char *content = row[0]; unsigned long contentLength = mysql_fetch_lengths(result)[0]; memcpy(preallocatedDestination, content, contentLength); mysql_free_result(result); filePartConnectionMutex.Unlock(); return contentLength; } mysql_free_result(result); } filePartConnectionMutex.Unlock(); return 0; } const int AutopatcherMySQLRepository::GetIncrementalReadChunkSize(void) const { // AutopatcherMySQLRepository::GetFilePart is extremely slow with larger files return 262144*4; }