TrinityCore
Loading...
Searching...
No Matches
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 "CryptoHash.h"
20#include "DBUpdater.h"
21#include "Field.h"
22#include "Log.h"
23#include "QueryResult.h"
24#include "Util.h"
25#include <boost/filesystem/directory.hpp>
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
52{
55 for (auto const& entry : directories)
56 FillFileListRecursively(entry.path, files, entry.state, 1);
57
58 return files;
59}
60
61void UpdateFetcher::FillFileListRecursively(Path const& path, LocaleFileStorage& storage, State const state, uint32 const depth) const
62{
63 static uint32 const MAX_DEPTH = 10;
64 static directory_iterator const end;
65
66 for (directory_iterator itr(path); itr != end; ++itr)
67 {
68 if (is_directory(itr->path()))
69 {
70 if (depth < MAX_DEPTH)
71 FillFileListRecursively(itr->path(), storage, state, depth + 1);
72 }
73 else if (itr->path().extension() == ".sql")
74 {
75 TC_LOG_TRACE("sql.updates", "Added locale file \"{}\".", itr->path().filename().generic_string());
76
77 LocaleFileEntry const entry = { itr->path(), state };
78
79 // Check for doubled filenames
80 // Because elements are only compared by their filenames, this is ok
81 if (storage.contains(entry))
82 {
83 TC_LOG_FATAL("sql.updates", "Duplicate filename \"{}\" occurred. Because updates are ordered " \
84 "by their filenames, every name needs to be unique!", itr->path().generic_string());
85
86 throw UpdateException("Updating failed, see the log for details.");
87 }
88
89 storage.insert(entry);
90 }
91 }
92}
93
95{
96 DirectoryStorage directories;
97
98 QueryResult const result = _retrieve("SELECT `path`, `state` FROM `updates_include`");
99 if (!result)
100 return directories;
101
102 do
103 {
104 Field* fields = result->Fetch();
105
106 std::string path = fields[0].GetString();
107 if (path.starts_with("$"))
108 path = _sourceDirectory->generic_string() + path.substr(1);
109
110 Path const p(path);
111
112 if (!is_directory(p))
113 {
114 TC_LOG_WARN("sql.updates", "DBUpdater: Given update include directory \"{}\" does not exist, skipped!", p.generic_string());
115 continue;
116 }
117
118 DirectoryEntry const entry = { p, AppliedFileEntry::StateConvert(fields[1].GetStringView()) };
119 directories.push_back(entry);
120
121 TC_LOG_TRACE("sql.updates", "Added applied file \"{}\" from remote.", p.filename().generic_string());
122
123 } while (result->NextRow());
124
125 return directories;
126}
127
129{
131
132 QueryResult result = _retrieve("SELECT `name`, `hash`, `state`, UNIX_TIMESTAMP(`timestamp`) FROM `updates` ORDER BY `name` ASC");
133 if (!result)
134 return map;
135
136 do
137 {
138 Field* fields = result->Fetch();
139
140 AppliedFileEntry const entry = { fields[0].GetString(), fields[1].GetString(),
141 AppliedFileEntry::StateConvert(fields[2].GetStringView()), fields[3].GetUInt64() };
142
143 map.insert(std::make_pair(entry.name, entry));
144 }
145 while (result->NextRow());
146
147 return map;
148}
149
150std::string UpdateFetcher::ReadSQLUpdate(boost::filesystem::path const& file) const
151{
152 std::ifstream in(file.c_str());
153 if (!in.is_open())
154 {
155 TC_LOG_FATAL("sql.updates", "Failed to open the sql update \"{}\" for reading! "
156 "Stopping the server to keep the database integrity, "
157 "try to identify and solve the issue or disable the database updater.",
158 file.generic_string());
159
160 throw UpdateException("Opening the sql update failed!");
161 }
162
163 auto update = [&in] {
164 std::ostringstream ss;
165 ss << in.rdbuf();
166 return ss.str();
167 }();
168
169 in.close();
170 return update;
171}
172
173UpdateResult UpdateFetcher::Update(bool const redundancyChecks,
174 bool const allowRehash,
175 bool const archivedRedundancy,
176 int32 const cleanDeadReferencesMaxCount) const
177{
178 LocaleFileStorage const available = GetFileList();
180
181 size_t countRecentUpdates = 0;
182 size_t countArchivedUpdates = 0;
183
184 // Count updates
185 for (auto const& [_, appliedFile] : applied)
186 if (appliedFile.state == RELEASED)
187 ++countRecentUpdates;
188 else
189 ++countArchivedUpdates;
190
191 // Fill hash to name cache
192 HashToFileNameStorage hashToName;
193 for (auto const& [name, appliedFile] : applied)
194 hashToName.try_emplace(appliedFile.hash, name);
195
196 size_t importedUpdates = 0;
197
198 for (auto const& availableQuery : available)
199 {
200 std::string availableQueryFilename = availableQuery.first.filename().string();
201
202 TC_LOG_DEBUG("sql.updates", "Checking update \"{}\"...", availableQueryFilename);
203
204 AppliedFileStorage::const_iterator iter = applied.find(availableQueryFilename);
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 = available.find(hashIter->second);
238
239 // Conflict!
240 if (localeIter != available.end())
241 {
242 TC_LOG_WARN("sql.updates", ">> It seems like the update \"{}\" \'{}\' was renamed, but the old file is still there! "
243 "Treating it as a new file! (It is probably an unmodified copy of the file \"{}\")",
244 availableQueryFilename, hash.substr(0, 7),
245 localeIter->first.filename().string());
246 }
247 // It is safe to treat the file as renamed here
248 else
249 {
250 TC_LOG_INFO("sql.updates", ">> Renaming update \"{}\" to \"{}\" \'{}\'.",
251 hashIter->second, availableQueryFilename, hash.substr(0, 7));
252
253 RenameEntry(hashIter->second, availableQueryFilename);
254 applied.erase(hashIter->second);
255 continue;
256 }
257 }
258 // Apply the update if it was never seen before.
259 else
260 {
261 TC_LOG_INFO("sql.updates", ">> Applying update \"{}\" \'{}\'...",
262 availableQueryFilename, hash.substr(0, 7));
263 }
264 }
265 // Rehash the update entry if it exists in our database with an empty hash.
266 else if (allowRehash && iter->second.hash.empty())
267 {
268 mode = MODE_REHASH;
269
270 TC_LOG_INFO("sql.updates", ">> Re-hashing update \"{}\" \'{}\'...", availableQueryFilename,
271 hash.substr(0, 7));
272 }
273 else
274 {
275 // If the hash of the files differs from the one stored in our database, reapply the update (because it changed).
276 if (iter->second.hash != hash)
277 {
278 TC_LOG_INFO("sql.updates", ">> Reapplying update \"{}\" \'{}\' -> \'{}\' (it changed)...", availableQueryFilename,
279 iter->second.hash.substr(0, 7), hash.substr(0, 7));
280 }
281 else
282 {
283 // If the file wasn't changed and just moved, update its state (if necessary).
284 if (iter->second.state != availableQuery.second)
285 {
286 TC_LOG_DEBUG("sql.updates", ">> Updating the state of \"{}\" to \'{}\'...",
287 availableQueryFilename, AppliedFileEntry::StateConvert(availableQuery.second));
288
289 UpdateState(availableQueryFilename, availableQuery.second);
290 }
291
292 TC_LOG_DEBUG("sql.updates", ">> Update is already applied and matches the hash \'{}\'.", hash.substr(0, 7));
293
294 applied.erase(iter);
295 continue;
296 }
297 }
298
299 uint32 speed = 0;
300 AppliedFileEntry const file = { availableQueryFilename, hash, availableQuery.second, 0 };
301
302 switch (mode)
303 {
304 case MODE_APPLY:
305 speed = Apply(availableQuery.first);
306 [[fallthrough]];
307 case MODE_REHASH:
308 UpdateEntry(file, speed);
309 break;
310 }
311
312 if (iter != applied.end())
313 applied.erase(iter);
314
315 if (mode == MODE_APPLY)
316 ++importedUpdates;
317 }
318
319 // Cleanup up orphaned entries (if enabled)
320 if (!applied.empty())
321 {
322 bool const doCleanup = (cleanDeadReferencesMaxCount < 0) || (applied.size() <= static_cast<size_t>(cleanDeadReferencesMaxCount));
323
324 for (auto const& entry : applied)
325 {
326 TC_LOG_WARN("sql.updates", ">> The file \'{}\' was applied to the database, but is missing in" \
327 " your update directory now!", entry.first);
328
329 if (doCleanup)
330 TC_LOG_INFO("sql.updates", "Deleting orphaned entry \'{}\'...", entry.first);
331 }
332
333 if (doCleanup)
334 CleanUp(applied);
335 else
336 {
337 TC_LOG_ERROR("sql.updates", "Cleanup is disabled! There were {} dirty files applied to your database, " \
338 "but they are now missing in your source directory!", applied.size());
339 }
340 }
341
342 return UpdateResult(importedUpdates, countRecentUpdates, countArchivedUpdates);
343}
344
346{
347 using Time = std::chrono::high_resolution_clock;
348
349 // Benchmark query speed
350 auto const begin = Time::now();
351
352 // Update database
353 _applyFile(path);
354
355 // Return the time it took the query to apply
356 return uint32(std::chrono::duration_cast<std::chrono::milliseconds>(Time::now() - begin).count());
357}
358
359void UpdateFetcher::UpdateEntry(AppliedFileEntry const& entry, uint32 const speed) const
360{
361 std::string const update = Trinity::StringFormat(R"(REPLACE INTO `updates` (`name`, `hash`, `state`, `speed`) VALUES ("{}", "{}", '{}', {}))",
362 entry.name, entry.hash, entry.GetStateAsString(), speed);
363
364 // Update database
365 _apply(update);
366}
367
368void UpdateFetcher::RenameEntry(std::string const& from, std::string const& to) const
369{
370 // Delete the target if it exists
371 {
372 std::string const update = "DELETE FROM `updates` WHERE `name`=\"" + to + "\"";
373
374 // Update database
375 _apply(update);
376 }
377
378 // Rename
379 {
380 std::string const update = "UPDATE `updates` SET `name`=\"" + to + "\" WHERE `name`=\"" + from + "\"";
381
382 // Update database
383 _apply(update);
384 }
385}
386
388{
389 if (storage.empty())
390 return;
391
392 std::stringstream update;
393 size_t remaining = storage.size();
394
395 update << "DELETE FROM `updates` WHERE `name` IN(";
396
397 for (auto const& entry : storage)
398 {
399 update << "\"" << entry.first << "\"";
400 if ((--remaining) > 0)
401 update << ", ";
402 }
403
404 update << ")";
405
406 // Update database
407 _apply(update.str());
408}
409
410void UpdateFetcher::UpdateState(std::string const& name, State const state) const
411{
412 std::string const update = Trinity::StringFormat(R"(UPDATE `updates` SET `state`='{}' WHERE `name`="{}")", AppliedFileEntry::StateConvert(state), name);
413
414 // Update database
415 _apply(update);
416}
417
419{
420 return arg.first.filename().string();
421}
std::shared_ptr< ResultSet > QueryResult
int32_t int32
Definition Define.h:150
uint32_t uint32
Definition Define.h:154
#define TC_LOG_DEBUG(filterType__, message__,...)
Definition Log.h:181
#define TC_LOG_ERROR(filterType__, message__,...)
Definition Log.h:190
#define TC_LOG_FATAL(filterType__, message__,...)
Definition Log.h:193
#define TC_LOG_INFO(filterType__, message__,...)
Definition Log.h:184
#define TC_LOG_WARN(filterType__, message__,...)
Definition Log.h:187
#define TC_LOG_TRACE(filterType__, message__,...)
Definition Log.h:178
std::string ByteArrayToHexStr(Container const &c, bool reverse=false)
Definition Util.h:431
Class used to access individual fields of database query result.
Definition Field.h:94
uint64 GetUInt64() const noexcept
Definition Field.cpp:71
std::string GetString() const noexcept
Definition Field.cpp:113
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
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
std::string StringFormat(FormatString< Args... > fmt, Args &&... args) noexcept
Default TC string format function.
STL namespace.
std::string_view GetStateAsString() const
static State StateConvert(std::string_view const &state)
DirectoryEntry(Path const &path_, State state_)
static std::string MakeComparisonObject(LocaleFileEntry const &arg)