TrinityCore
UpdateFetcher.cpp
Go to the documentation of this file.
1/*
2 * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
3 *
4 * This program is free software; you can redistribute it and/or modify it
5 * under the terms of the GNU General Public License as published by the
6 * Free Software Foundation; either version 2 of the License, or (at your
7 * option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12 * more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18#include "UpdateFetcher.h"
19#include "Common.h"
20#include "DBUpdater.h"
21#include "Field.h"
22#include "CryptoHash.h"
23#include "Log.h"
24#include "QueryResult.h"
25#include "Util.h"
26#include <boost/filesystem/operations.hpp>
27#include <fstream>
28#include <sstream>
29
30using namespace boost::filesystem;
31
33{
34 DirectoryEntry(Path const& path_, State state_) : path(path_), state(state_) { }
35
36 Path const path;
37 State const state;
38};
39
40UpdateFetcher::UpdateFetcher(Path const& sourceDirectory,
41 std::function<void(std::string const&)> const& apply,
42 std::function<void(Path const& path)> const& applyFile,
43 std::function<QueryResult(std::string const&)> const& retrieve) :
44 _sourceDirectory(std::make_unique<Path>(sourceDirectory)), _apply(apply), _applyFile(applyFile),
45 _retrieve(retrieve)
46{
47}
48
50{
51}
52
54{
57 for (auto const& entry : directories)
58 FillFileListRecursively(entry.path, files, entry.state, 1);
59
60 return files;
61}
62
63void UpdateFetcher::FillFileListRecursively(Path const& path, LocaleFileStorage& storage, State const state, uint32 const depth) const
64{
65 static uint32 const MAX_DEPTH = 10;
66 static directory_iterator const end;
67
68 for (directory_iterator itr(path); itr != end; ++itr)
69 {
70 if (is_directory(itr->path()))
71 {
72 if (depth < MAX_DEPTH)
73 FillFileListRecursively(itr->path(), storage, state, depth + 1);
74 }
75 else if (itr->path().extension() == ".sql")
76 {
77 TC_LOG_TRACE("sql.updates", "Added locale file \"{}\".", itr->path().filename().generic_string());
78
79 LocaleFileEntry const entry = { itr->path(), state };
80
81 // Check for doubled filenames
82 // Because elements are only compared by their filenames, this is ok
83 if (storage.find(entry) != storage.end())
84 {
85 TC_LOG_FATAL("sql.updates", "Duplicate filename \"{}\" occurred. Because updates are ordered " \
86 "by their filenames, every name needs to be unique!", itr->path().generic_string());
87
88 throw UpdateException("Updating failed, see the log for details.");
89 }
90
91 storage.insert(entry);
92 }
93 }
94}
95
97{
98 DirectoryStorage directories;
99
100 QueryResult const result = _retrieve("SELECT `path`, `state` FROM `updates_include`");
101 if (!result)
102 return directories;
103
104 do
105 {
106 Field* fields = result->Fetch();
107
108 std::string path = fields[0].GetString();
109 if (path.substr(0, 1) == "$")
110 path = _sourceDirectory->generic_string() + path.substr(1);
111
112 Path const p(path);
113
114 if (!is_directory(p))
115 {
116 TC_LOG_WARN("sql.updates", "DBUpdater: Given update include directory \"{}\" does not exist, skipped!", p.generic_string());
117 continue;
118 }
119
120 DirectoryEntry const entry = { p, AppliedFileEntry::StateConvert(fields[1].GetString()) };
121 directories.push_back(entry);
122
123 TC_LOG_TRACE("sql.updates", "Added applied file \"{}\" from remote.", p.filename().generic_string());
124
125 } while (result->NextRow());
126
127 return directories;
128}
129
131{
133
134 QueryResult result = _retrieve("SELECT `name`, `hash`, `state`, UNIX_TIMESTAMP(`timestamp`) FROM `updates` ORDER BY `name` ASC");
135 if (!result)
136 return map;
137
138 do
139 {
140 Field* fields = result->Fetch();
141
142 AppliedFileEntry const entry = { fields[0].GetString(), fields[1].GetString(),
143 AppliedFileEntry::StateConvert(fields[2].GetString()), fields[3].GetUInt64() };
144
145 map.insert(std::make_pair(entry.name, entry));
146 }
147 while (result->NextRow());
148
149 return map;
150}
151
152std::string UpdateFetcher::ReadSQLUpdate(boost::filesystem::path const& file) const
153{
154 std::ifstream in(file.c_str());
155 if (!in.is_open())
156 {
157 TC_LOG_FATAL("sql.updates", "Failed to open the sql update \"{}\" for reading! "
158 "Stopping the server to keep the database integrity, "
159 "try to identify and solve the issue or disable the database updater.",
160 file.generic_string());
161
162 throw UpdateException("Opening the sql update failed!");
163 }
164
165 auto update = [&in] {
166 std::ostringstream ss;
167 ss << in.rdbuf();
168 return ss.str();
169 }();
170
171 in.close();
172 return update;
173}
174
175UpdateResult UpdateFetcher::Update(bool const redundancyChecks,
176 bool const allowRehash,
177 bool const archivedRedundancy,
178 int32 const cleanDeadReferencesMaxCount) const
179{
180 LocaleFileStorage const available = GetFileList();
182
183 size_t countRecentUpdates = 0;
184 size_t countArchivedUpdates = 0;
185
186 // Count updates
187 for (auto const& entry : applied)
188 if (entry.second.state == RELEASED)
189 ++countRecentUpdates;
190 else
191 ++countArchivedUpdates;
192
193 // Fill hash to name cache
194 HashToFileNameStorage hashToName;
195 for (auto entry : applied)
196 hashToName.insert(std::make_pair(entry.second.hash, entry.first));
197
198 size_t importedUpdates = 0;
199
200 for (auto const& availableQuery : available)
201 {
202 TC_LOG_DEBUG("sql.updates", "Checking update \"{}\"...", availableQuery.first.filename().generic_string());
203
204 AppliedFileStorage::const_iterator iter = applied.find(availableQuery.first.filename().string());
205 if (iter != applied.end())
206 {
207 // If redundancy is disabled, skip it, because the update is already applied.
208 if (!redundancyChecks)
209 {
210 TC_LOG_DEBUG("sql.updates", ">> Update is already applied, skipping redundancy checks.");
211 applied.erase(iter);
212 continue;
213 }
214
215 // If the update is in an archived directory and is marked as archived in our database, skip redundancy checks (archived updates never change).
216 if (!archivedRedundancy && (iter->second.state == ARCHIVED) && (availableQuery.second == ARCHIVED))
217 {
218 TC_LOG_DEBUG("sql.updates", ">> Update is archived and marked as archived in database, skipping redundancy checks.");
219 applied.erase(iter);
220 continue;
221 }
222 }
223
224 // Calculate a Sha1 hash based on query content.
225 std::string const hash = ByteArrayToHexStr(Trinity::Crypto::SHA1::GetDigestOf(ReadSQLUpdate(availableQuery.first)));
226
227 UpdateMode mode = MODE_APPLY;
228
229 // Update is not in our applied list
230 if (iter == applied.end())
231 {
232 // Catch renames (different filename, but same hash)
233 HashToFileNameStorage::const_iterator const hashIter = hashToName.find(hash);
234 if (hashIter != hashToName.end())
235 {
236 // Check if the original file was removed. If not, we've got a problem.
237 LocaleFileStorage::const_iterator localeIter;
238 // Push localeIter forward
239 for (localeIter = available.begin(); (localeIter != available.end()) &&
240 (localeIter->first.filename().string() != hashIter->second); ++localeIter);
241
242 // Conflict!
243 if (localeIter != available.end())
244 {
245 TC_LOG_WARN("sql.updates", ">> It seems like the update \"{}\" \'{}\' was renamed, but the old file is still there! " \
246 "Treating it as a new file! (It is probably an unmodified copy of the file \"{}\")",
247 availableQuery.first.filename().string(), hash.substr(0, 7),
248 localeIter->first.filename().string());
249 }
250 // It is safe to treat the file as renamed here
251 else
252 {
253 TC_LOG_INFO("sql.updates", ">> Renaming update \"{}\" to \"{}\" \'{}\'.",
254 hashIter->second, availableQuery.first.filename().string(), hash.substr(0, 7));
255
256 RenameEntry(hashIter->second, availableQuery.first.filename().string());
257 applied.erase(hashIter->second);
258 continue;
259 }
260 }
261 // Apply the update if it was never seen before.
262 else
263 {
264 TC_LOG_INFO("sql.updates", ">> Applying update \"{}\" \'{}\'...",
265 availableQuery.first.filename().string(), hash.substr(0, 7));
266 }
267 }
268 // Rehash the update entry if it exists in our database with an empty hash.
269 else if (allowRehash && iter->second.hash.empty())
270 {
271 mode = MODE_REHASH;
272
273 TC_LOG_INFO("sql.updates", ">> Re-hashing update \"{}\" \'{}\'...", availableQuery.first.filename().string(),
274 hash.substr(0, 7));
275 }
276 else
277 {
278 // If the hash of the files differs from the one stored in our database, reapply the update (because it changed).
279 if (iter->second.hash != hash)
280 {
281 TC_LOG_INFO("sql.updates", ">> Reapplying update \"{}\" \'{}\' -> \'{}\' (it changed)...", availableQuery.first.filename().string(),
282 iter->second.hash.substr(0, 7), hash.substr(0, 7));
283 }
284 else
285 {
286 // If the file wasn't changed and just moved, update its state (if necessary).
287 if (iter->second.state != availableQuery.second)
288 {
289 TC_LOG_DEBUG("sql.updates", ">> Updating the state of \"{}\" to \'{}\'...",
290 availableQuery.first.filename().string(), AppliedFileEntry::StateConvert(availableQuery.second));
291
292 UpdateState(availableQuery.first.filename().string(), availableQuery.second);
293 }
294
295 TC_LOG_DEBUG("sql.updates", ">> Update is already applied and matches the hash \'{}\'.", hash.substr(0, 7));
296
297 applied.erase(iter);
298 continue;
299 }
300 }
301
302 uint32 speed = 0;
303 AppliedFileEntry const file = { availableQuery.first.filename().string(), hash, availableQuery.second, 0 };
304
305 switch (mode)
306 {
307 case MODE_APPLY:
308 speed = Apply(availableQuery.first);
309 [[fallthrough]];
310 case MODE_REHASH:
311 UpdateEntry(file, speed);
312 break;
313 }
314
315 if (iter != applied.end())
316 applied.erase(iter);
317
318 if (mode == MODE_APPLY)
319 ++importedUpdates;
320 }
321
322 // Cleanup up orphaned entries (if enabled)
323 if (!applied.empty())
324 {
325 bool const doCleanup = (cleanDeadReferencesMaxCount < 0) || (applied.size() <= static_cast<size_t>(cleanDeadReferencesMaxCount));
326
327 for (auto const& entry : applied)
328 {
329 TC_LOG_WARN("sql.updates", ">> The file \'{}\' was applied to the database, but is missing in" \
330 " your update directory now!", entry.first);
331
332 if (doCleanup)
333 TC_LOG_INFO("sql.updates", "Deleting orphaned entry \'{}\'...", entry.first);
334 }
335
336 if (doCleanup)
337 CleanUp(applied);
338 else
339 {
340 TC_LOG_ERROR("sql.updates", "Cleanup is disabled! There were {} dirty files applied to your database, " \
341 "but they are now missing in your source directory!", applied.size());
342 }
343 }
344
345 return UpdateResult(importedUpdates, countRecentUpdates, countArchivedUpdates);
346}
347
349{
350 using Time = std::chrono::high_resolution_clock;
351
352 // Benchmark query speed
353 auto const begin = Time::now();
354
355 // Update database
356 _applyFile(path);
357
358 // Return the time it took the query to apply
359 return uint32(std::chrono::duration_cast<std::chrono::milliseconds>(Time::now() - begin).count());
360}
361
362void UpdateFetcher::UpdateEntry(AppliedFileEntry const& entry, uint32 const speed) const
363{
364 std::string const update = "REPLACE INTO `updates` (`name`, `hash`, `state`, `speed`) VALUES (\"" +
365 entry.name + "\", \"" + entry.hash + "\", \'" + entry.GetStateAsString() + "\', " + std::to_string(speed) + ")";
366
367 // Update database
368 _apply(update);
369}
370
371void UpdateFetcher::RenameEntry(std::string const& from, std::string const& to) const
372{
373 // Delete the target if it exists
374 {
375 std::string const update = "DELETE FROM `updates` WHERE `name`=\"" + to + "\"";
376
377 // Update database
378 _apply(update);
379 }
380
381 // Rename
382 {
383 std::string const update = "UPDATE `updates` SET `name`=\"" + to + "\" WHERE `name`=\"" + from + "\"";
384
385 // Update database
386 _apply(update);
387 }
388}
389
391{
392 if (storage.empty())
393 return;
394
395 std::stringstream update;
396 size_t remaining = storage.size();
397
398 update << "DELETE FROM `updates` WHERE `name` IN(";
399
400 for (auto const& entry : storage)
401 {
402 update << "\"" << entry.first << "\"";
403 if ((--remaining) > 0)
404 update << ", ";
405 }
406
407 update << ")";
408
409 // Update database
410 _apply(update.str());
411}
412
413void UpdateFetcher::UpdateState(std::string const& name, State const state) const
414{
415 std::string const update = "UPDATE `updates` SET `state`=\'" + AppliedFileEntry::StateConvert(state) + "\' WHERE `name`=\"" + name + "\"";
416
417 // Update database
418 _apply(update);
419}
420
422{
423 return left.first.filename().string() < right.first.filename().string();
424}
std::shared_ptr< ResultSet > QueryResult
int32_t int32
Definition: Define.h:138
uint32_t uint32
Definition: Define.h:142
#define TC_LOG_WARN(filterType__,...)
Definition: Log.h:162
#define TC_LOG_DEBUG(filterType__,...)
Definition: Log.h:156
#define TC_LOG_TRACE(filterType__,...)
Definition: Log.h:153
#define TC_LOG_ERROR(filterType__,...)
Definition: Log.h:165
#define TC_LOG_INFO(filterType__,...)
Definition: Log.h:159
#define TC_LOG_FATAL(filterType__,...)
Definition: Log.h:168
std::string ByteArrayToHexStr(Container const &c, bool reverse=false)
Definition: Util.h:368
Class used to access individual fields of database query result.
Definition: Field.h:90
std::string GetString() const
Definition: Field.cpp:118
uint64 GetUInt64() const
Definition: Field.cpp:78
static Digest GetDigestOf(uint8 const *data, size_t len)
Definition: CryptoHash.h:49
uint32 Apply(Path const &path) const
std::function< void(std::string const &)> const _apply
std::unordered_map< std::string, AppliedFileEntry > AppliedFileStorage
std::unique_ptr< Path > const _sourceDirectory
LocaleFileStorage GetFileList() const
void UpdateState(std::string const &name, State const state) const
UpdateFetcher(Path const &updateDirectory, std::function< void(std::string const &)> const &apply, std::function< void(Path const &path)> const &applyFile, std::function< QueryResult(std::string const &)> const &retrieve)
std::set< LocaleFileEntry, PathCompare > LocaleFileStorage
std::string ReadSQLUpdate(Path const &file) const
boost::filesystem::path Path
Definition: UpdateFetcher.h:51
std::function< void(Path const &path)> const _applyFile
UpdateResult Update(bool const redundancyChecks, bool const allowRehash, bool const archivedRedundancy, int32 const cleanDeadReferencesMaxCount) const
void UpdateEntry(AppliedFileEntry const &entry, uint32 const speed=0) const
void FillFileListRecursively(Path const &path, LocaleFileStorage &storage, State const state, uint32 const depth) const
std::vector< UpdateFetcher::DirectoryEntry > DirectoryStorage
void RenameEntry(std::string const &from, std::string const &to) const
std::unordered_map< std::string, std::string > HashToFileNameStorage
AppliedFileStorage ReceiveAppliedFiles() const
void CleanUp(AppliedFileStorage const &storage) const
std::pair< Path, State > LocaleFileEntry
std::function< QueryResult(std::string const &)> const _retrieve
DirectoryStorage ReceiveIncludedDirectories() const
void apply(T *val)
Definition: ByteConverter.h:41
STL namespace.
std::string GetStateAsString() const
Definition: UpdateFetcher.h:99
static State StateConvert(std::string const &state)
Definition: UpdateFetcher.h:89
DirectoryEntry(Path const &path_, State state_)
bool operator()(LocaleFileEntry const &left, LocaleFileEntry const &right) const