1 /*
2  * Copyright (C) 2017-2018 Red Hat, Inc.
3  *
4  * Licensed under the GNU Lesser General Public License Version 2.1
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19  */
20 
21 #include <algorithm>
22 #include <cstdio>
23 #include <cstring>
24 #include <ctime>
25 #include <dirent.h>
26 #include <fstream>
27 #include <functional>
28 #include <map>
29 #include <memory>
30 #include <stdexcept>
31 #include <string>
32 #include <map>
33 #include <vector>
34 #include <sstream>
35 
36 #include "../utils/bgettext/bgettext-lib.h"
37 #include "../utils/filesystem.hpp"
38 #include "../utils/utils.hpp"
39 
40 #include "RPMItem.hpp"
41 #include "Swdb.hpp"
42 #include "Transaction.hpp"
43 #include "TransactionItem.hpp"
44 #include "Transformer.hpp"
45 
46 namespace libdnf {
47 
48 static const char *sql_create_tables =
49 #include "sql/create_tables.sql"
50     ;
51 
52 static const char * const sql_migrate_tables_1_2 =
53 #include "sql/migrate_tables_1_2.sql"
54     ;
55 
56 void
createDatabase(SQLite3Ptr conn)57 Transformer::createDatabase(SQLite3Ptr conn)
58 {
59     conn->exec(sql_create_tables);
60     Transformer::migrateSchema(conn);
61 }
62 
63 void
migrateSchema(SQLite3Ptr conn)64 Transformer::migrateSchema(SQLite3Ptr conn)
65 {
66     // read schema version
67     SQLite3::Query query(*conn, "select value from config where key = 'version';");
68     if (query.step() == SQLite3::Statement::StepResult::ROW){
69         auto schemaVersion = query.get<std::string>("value");
70 
71         if (schemaVersion == "1.1") {
72             conn->exec(sql_migrate_tables_1_2);
73         }
74     }
75     else {
76         throw Exception(_("Database Corrupted: no row 'version' in table 'config'"));
77     }
78 }
79 
80 /**
81  * Map of supported actions (originally states): string -> enum
82  */
83 static const std::map<std::string, TransactionItemAction > actions = {
84     {"Install", TransactionItemAction::INSTALL},
85     {"True-Install", TransactionItemAction::INSTALL},
86     {"Dep-Install", TransactionItemAction::INSTALL},
87     {"Downgrade", TransactionItemAction::DOWNGRADE},
88     {"Downgraded", TransactionItemAction::DOWNGRADED},
89     {"Obsoleting", TransactionItemAction::OBSOLETE},
90     {"Obsoleted", TransactionItemAction::OBSOLETED},
91     {"Update", TransactionItemAction::UPGRADE},
92     {"Updated", TransactionItemAction::UPGRADED},
93     {"Erase", TransactionItemAction::REMOVE},
94     {"Reinstall", TransactionItemAction::REINSTALL},
95     {"Reinstalled", TransactionItemAction::REINSTALL}};
96 
97 /**
98  * Map of supported reasons: string -> enum
99  */
100 static const std::map< std::string, TransactionItemReason > reasons = {
101     {"dep", TransactionItemReason::DEPENDENCY},
102     {"user", TransactionItemReason::USER},
103     {"clean", TransactionItemReason::CLEAN},
104     {"weak", TransactionItemReason::WEAK_DEPENDENCY},
105     {"group", TransactionItemReason::GROUP}};
106 
107 /**
108  * Convert string reason into appropriate enumerated variant
109  */
110 TransactionItemReason
getReason(const std::string & reason)111 Transformer::getReason(const std::string &reason)
112 {
113     auto it = reasons.find(reason);
114     if (it == reasons.end()) {
115         return TransactionItemReason::UNKNOWN;
116     }
117     return it->second;
118 }
119 
120 /**
121  * Default constructor of the Transformer object
122  * \param outputFile path to output SQLite3 database
123  * \param inputDir directory to load data from (e.g. `/var/lib/dnf/`)
124  */
Transformer(const std::string & inputDir,const std::string & outputFile)125 Transformer::Transformer(const std::string &inputDir, const std::string &outputFile)
126   : inputDir(inputDir)
127   , outputFile(outputFile)
128 {
129 }
130 
131 /**
132  * Perform the database transformation routine.
133  * The database is transformed in-memory.
134  * Final scheme is dumped into outputFile
135  */
136 void
transform()137 Transformer::transform()
138 {
139     auto swdb = std::make_shared< SQLite3 >(":memory:");
140 
141     if (pathExists(outputFile.c_str())) {
142         throw std::runtime_error("DB file already exists:" + outputFile);
143     }
144 
145     // create directory path if necessary
146     makeDirPath(outputFile);
147 
148     // create a new database file
149     createDatabase(swdb);
150 
151     // migrate history db if it exists
152     try {
153         // make a copy of source database to make creating indexes temporary
154         auto history = std::make_shared< SQLite3 >(":memory:");
155         history->restore(historyPath().c_str());
156 
157         // create additional indexes in the source database to increase conversion speed
158         history->exec("CREATE INDEX IF NOT EXISTS i_trans_cmdline_tid ON trans_cmdline(tid);");
159         history->exec("CREATE INDEX IF NOT EXISTS i_trans_data_pkgs_tid ON trans_data_pkgs(tid);");
160         history->exec("CREATE INDEX IF NOT EXISTS i_trans_script_stdout_tid ON trans_script_stdout(tid);");
161         history->exec("CREATE INDEX IF NOT EXISTS i_trans_with_pkgs_tid_pkgtupid ON trans_with_pkgs(tid, pkgtupid);");
162 
163         // transform objects
164         transformTrans(swdb, history);
165 
166         // transform groups
167         transformGroups(swdb);
168     }
169     catch (Exception &) {
170         // TODO: use a different (more specific) exception
171     }
172 
173     // dump database to a file
174     swdb->backup(outputFile);
175 }
176 
177 /**
178  * Transform transactions from the history database
179  * \param swdb pointer to swdb SQLite3 object
180  * \param swdb pointer to history database SQLite3 object
181  */
182 void
transformTrans(SQLite3Ptr swdb,SQLite3Ptr history)183 Transformer::transformTrans(SQLite3Ptr swdb, SQLite3Ptr history)
184 {
185     std::vector< std::shared_ptr< TransformerTransaction > > result;
186 
187     // we need to left join with trans_cmdline
188     // there is no cmdline for certain transactions (e.g. 1)
189     const char *trans_sql = R"**(
190         SELECT
191             tb.tid as id,
192             tb.timestamp as dt_begin,
193             tb.rpmdb_version rpmdb_version_begin,
194             tb.loginuid as user_id,
195             te.timestamp as dt_end,
196             te.rpmdb_version as rpmdb_version_end,
197             te.return_code as state,
198             tc.cmdline as cmdline
199         FROM
200             trans_beg tb
201             JOIN trans_end te using(tid)
202             LEFT JOIN trans_cmdline tc using(tid)
203         ORDER BY
204             tb.tid
205     )**";
206 
207     const char *releasever_sql = R"**(
208         SELECT DISTINCT
209             trans_data_pkgs.tid as tid,
210             yumdb_val as releasever
211         FROM
212             trans_data_pkgs
213         JOIN
214             pkg_yumdb USING (pkgtupid)
215         WHERE
216             yumdb_key='releasever'
217     )**";
218 
219     // get release version for all the transactions
220     std::map< int64_t, std::string > releasever;
221     SQLite3::Query releasever_query(*history.get(), releasever_sql);
222     while (releasever_query.step() == SQLite3::Statement::StepResult::ROW) {
223         std::string releaseVerStr = releasever_query.get< std::string >("releasever");
224         releasever[releasever_query.get< int64_t >("tid")] = releaseVerStr;
225     }
226 
227     // iterate over history transactions
228     SQLite3::Query query(*history.get(), trans_sql);
229     while (query.step() == SQLite3::Statement::StepResult::ROW) {
230         auto trans = std::make_shared< TransformerTransaction >(swdb);
231         trans->setId(query.get< int >("id"));
232         trans->setDtBegin(query.get< int64_t >("dt_begin"));
233         trans->setDtEnd(query.get< int64_t >("dt_end"));
234         trans->setRpmdbVersionBegin(query.get< std::string >("rpmdb_version_begin"));
235         trans->setRpmdbVersionEnd(query.get< std::string >("rpmdb_version_end"));
236 
237         // set release version if available
238         auto it = releasever.find(trans->getId());
239         if (it != releasever.end()) {
240             trans->setReleasever(it->second);
241         }
242 
243         trans->setUserId(query.get< int >("user_id"));
244         trans->setCmdline(query.get< std::string >("cmdline"));
245 
246         TransactionState state = query.get< int >("state") == 0 ? TransactionState::DONE : TransactionState::ERROR;
247 
248         transformRPMItems(swdb, history, trans);
249         transformTransWith(swdb, history, trans);
250 
251         trans->begin();
252 
253         transformOutput(history, trans);
254 
255         trans->finish(state);
256     }
257 }
258 
259 static void
fillRPMItem(std::shared_ptr<RPMItem> rpm,SQLite3::Query & query)260 fillRPMItem(std::shared_ptr< RPMItem > rpm, SQLite3::Query &query)
261 {
262     rpm->setName(query.get< std::string >("name"));
263     rpm->setEpoch(query.get< int64_t >("epoch"));
264     rpm->setVersion(query.get< std::string >("version"));
265     rpm->setRelease(query.get< std::string >("release"));
266     rpm->setArch(query.get< std::string >("arch"));
267     rpm->save();
268 }
269 
270 /**
271  * Transform binding between a Transaction and packages, which performed the transaction.
272  * \param swdb pointer to swdb SQLite3 object
273  * \param swdb pointer to history database SQLite3 object
274  */
275 void
transformTransWith(SQLite3Ptr swdb,SQLite3Ptr history,std::shared_ptr<TransformerTransaction> trans)276 Transformer::transformTransWith(SQLite3Ptr swdb,
277                                 SQLite3Ptr history,
278                                 std::shared_ptr< TransformerTransaction > trans)
279 {
280     const char *sql = R"**(
281         SELECT
282             name,
283             epoch,
284             version,
285             release,
286             arch
287         FROM
288             trans_with_pkgs
289             JOIN pkgtups using (pkgtupid)
290         WHERE
291             tid=?
292     )**";
293 
294     // transform stdout
295     SQLite3::Query query(*history.get(), sql);
296     query.bindv(trans->getId());
297     while (query.step() == SQLite3::Statement::StepResult::ROW) {
298         // create RPM item object
299         auto rpm = std::make_shared< RPMItem >(swdb);
300         fillRPMItem(rpm, query);
301         trans->addSoftwarePerformedWith(rpm);
302     }
303 }
304 
305 /**
306  * Transform transaction console outputs.
307  * \param swdb pointer to history database SQLite3 object
308  */
309 void
transformOutput(SQLite3Ptr history,std::shared_ptr<TransformerTransaction> trans)310 Transformer::transformOutput(SQLite3Ptr history, std::shared_ptr< TransformerTransaction > trans)
311 {
312     const char *sql = R"**(
313         SELECT
314             line
315         FROM
316             trans_script_stdout
317         WHERE
318             tid = ?
319         ORDER BY
320             lid
321     )**";
322 
323     // transform stdout
324     SQLite3::Query query(*history.get(), sql);
325     query.bindv(trans->getId());
326     while (query.step() == SQLite3::Statement::StepResult::ROW) {
327         trans->addConsoleOutputLine(1, query.get< std::string >("line"));
328     }
329 
330     sql = R"**(
331         SELECT
332             msg
333         FROM
334             trans_error
335         WHERE
336             tid = ?
337         ORDER BY
338             mid
339     )**";
340 
341     // transform stderr
342     SQLite3::Query errorQuery(*history.get(), sql);
343     errorQuery.bindv(trans->getId());
344     while (errorQuery.step() == SQLite3::Statement::StepResult::ROW) {
345         trans->addConsoleOutputLine(2, errorQuery.get< std::string >("msg"));
346     }
347 }
348 
349 static void
getYumdbData(int64_t itemId,SQLite3Ptr history,TransactionItemReason & reason,std::string & repoid)350 getYumdbData(int64_t itemId, SQLite3Ptr history, TransactionItemReason &reason, std::string &repoid)
351 {
352     const char *sql = R"**(
353         SELECT
354             yumdb_key as key,
355             yumdb_val as value
356         FROM
357             pkg_yumdb
358         WHERE
359             pkgtupid=?
360             and key IN ('reason', 'from_repo')
361     )**";
362 
363     // load reason and repoid data from yumdb
364     SQLite3::Query query(*history.get(), sql);
365     query.bindv(itemId);
366     while (query.step() == SQLite3::Statement::StepResult::ROW) {
367         std::string key = query.get< std::string >("key");
368         if (key == "reason") {
369             reason = Transformer::getReason(query.get< std::string >("value"));
370         } else if (key == "from_repo") {
371             repoid = query.get< std::string >("value");
372         }
373     }
374 }
375 
376 /**
377  * Transform RPM Items from a particular transaction.
378  * \param swdb pointer to swdb SQLite3 object
379  * \param swdb pointer to history database SQLite3 objects
380  * \param trans Transaction whose items should be transformed
381  */
382 void
transformRPMItems(SQLite3Ptr swdb,SQLite3Ptr history,std::shared_ptr<TransformerTransaction> trans)383 Transformer::transformRPMItems(SQLite3Ptr swdb,
384                                SQLite3Ptr history,
385                                std::shared_ptr< TransformerTransaction > trans)
386 {
387     // the order is important here - its Update, Updated
388     const char *pkg_sql = R"**(
389         SELECT
390             t.state,
391             t.done,
392             r.pkgtupid as id,
393             r.name,
394             r.epoch,
395             r.version,
396             r.release,
397             r.arch
398         FROM
399             trans_data_pkgs t
400             JOIN pkgtups r using(pkgtupid)
401         WHERE
402             t.tid=?
403     )**";
404 
405     SQLite3::Query query(*history.get(), pkg_sql);
406     query.bindv(trans->getId());
407 
408     TransactionItemPtr last = nullptr;
409 
410     /*
411      * Item in a single transaction can be both Obsoleted multiple times and Updated.
412      * We need to keep track of all the obsoleted items,
413      * so we can promote them to Updated in case.
414      * Obsoleted records will be kept in item_replaced table,
415      * so it's always obvious, that particular package was both Obsoleted
416      * and Updated. Technically, we could replace action Obsoleted with action Erase.
417      */
418     std::map< int64_t, TransactionItemPtr > obsoletedItems;
419 
420     // iterate over transaction packages in the history database
421     while (query.step() == SQLite3::Statement::StepResult::ROW) {
422 
423         // create RPM item object
424         auto rpm = std::make_shared< RPMItem >(swdb);
425         fillRPMItem(rpm, query);
426 
427         // get item state/action
428         std::string stateString = query.get< std::string >("state");
429         TransactionItemAction action = actions.at(stateString);
430 
431         // `Obsoleting` record is duplicated with previous record (with different action)
432         if (action == TransactionItemAction::OBSOLETE) {
433             continue;
434         }
435 
436         // find out if an item was previously obsoleted
437         auto pastObsoleted = obsoletedItems.find(rpm->getId());
438 
439         TransactionItemPtr transItem = nullptr;
440 
441         if (pastObsoleted == obsoletedItems.end()) {
442             // item hasn't been obsoleted yet
443 
444             // load reason and from_repo
445             TransactionItemReason reason = TransactionItemReason::UNKNOWN;
446             std::string repoid;
447             getYumdbData(query.get< int64_t >("id"), history, reason, repoid);
448 
449             // add TransactionItem object
450             transItem = trans->addItem(rpm, repoid, action, reason);
451             transItem->setState(query.get< std::string >("done") == "TRUE" ? TransactionItemState::DONE : TransactionItemState::ERROR);
452         } else {
453             // item has been obsoleted - we just need to update the action
454             transItem = pastObsoleted->second;
455             transItem->setAction(action);
456         }
457 
458         // resolve replaced by
459         switch (action) {
460             case TransactionItemAction::OBSOLETED:
461                 obsoletedItems[rpm->getId()] = transItem;
462                 transItem->addReplacedBy(last);
463                 break;
464             case TransactionItemAction::DOWNGRADED:
465             case TransactionItemAction::UPGRADED:
466                 transItem->addReplacedBy(last);
467                 break;
468             default:
469                 break;
470         }
471 
472         // keep the last item in case of obsoletes
473         last = transItem;
474     }
475 }
476 
477 /**
478  * Construct CompsGroupItem object from JSON
479  * \param group group json object
480  */
481 CompsGroupItemPtr
processGroup(SQLite3Ptr swdb,const char * groupId,struct json_object * group)482 Transformer::processGroup(SQLite3Ptr swdb, const char *groupId, struct json_object *group)
483 {
484     struct json_object *value;
485 
486     // create group
487     auto compsGroup = std::make_shared< CompsGroupItem >(swdb);
488 
489     compsGroup->setGroupId(groupId);
490 
491     if (json_object_object_get_ex(group, "name", &value)) {
492         compsGroup->setName(json_object_get_string(value));
493     }
494 
495     if (json_object_object_get_ex(group, "ui_name", &value)) {
496         compsGroup->setTranslatedName(json_object_get_string(value));
497     }
498 
499     // TODO parse pkg_types to CompsPackageType
500     if (json_object_object_get_ex(group, "full_list", &value)) {
501         int len = json_object_array_length(value);
502         for (int i = 0; i < len; ++i) {
503             const char *key = json_object_get_string(json_object_array_get_idx(value, i));
504             compsGroup->addPackage(key, true, CompsPackageType::MANDATORY);
505         }
506     }
507 
508     // TODO parse pkg_types to CompsPackageType
509     if (json_object_object_get_ex(group, "pkg_exclude", &value)) {
510         int len = json_object_array_length(value);
511         for (int i = 0; i < len; ++i) {
512             const char *key = json_object_get_string(json_object_array_get_idx(value, i));
513             compsGroup->addPackage(key, false, CompsPackageType::MANDATORY);
514         }
515     }
516 
517     compsGroup->save();
518     return compsGroup;
519 }
520 
521 /**
522  * Construct CompsEnvironmentItem object from JSON
523  * \param env environment json object
524  */
525 std::shared_ptr< CompsEnvironmentItem >
processEnvironment(SQLite3Ptr swdb,const char * envId,struct json_object * env)526 Transformer::processEnvironment(SQLite3Ptr swdb, const char *envId, struct json_object *env)
527 {
528     struct json_object *value;
529 
530     // create environment
531     auto compsEnv = std::make_shared< CompsEnvironmentItem >(swdb);
532     compsEnv->setEnvironmentId(envId);
533 
534     if (json_object_object_get_ex (env, "name", &value)) {
535         compsEnv->setName(json_object_get_string(value));
536     }
537 
538     if (json_object_object_get_ex (env, "ui_name", &value)) {
539         compsEnv->setTranslatedName(json_object_get_string(value));
540     }
541 
542     // TODO parse pkg_types/grp_types to CompsPackageType
543     if (json_object_object_get_ex(env, "full_list", &value)) {
544         int len = json_object_array_length(value);
545         for (int i = 0; i < len; ++i) {
546             const char *key = json_object_get_string(json_object_array_get_idx(value, i));
547             compsEnv->addGroup(key, true, CompsPackageType::MANDATORY);
548         }
549     }
550 
551     // TODO parse pkg_types/grp_types to CompsPackageType
552     if (json_object_object_get_ex(env, "pkg_exclude", &value)) {
553         int len = json_object_array_length(value);
554         for (int i = 0; i < len; ++i) {
555             const char *key = json_object_get_string(json_object_array_get_idx(value, i));
556             compsEnv->addGroup(key, false, CompsPackageType::MANDATORY);
557         }
558     }
559 
560     compsEnv->save();
561 
562     return compsEnv;
563 }
564 
565 /**
566  * Create fake transaction for groups in persistor
567  * \param swdb pointer to swdb SQLite3 object
568  * \param root group persistor root node
569  */
570 void
processGroupPersistor(SQLite3Ptr swdb,struct json_object * root)571 Transformer::processGroupPersistor(SQLite3Ptr swdb, struct json_object *root)
572 {
573     // there is no rpmdb change in this transaction,
574     // use rpmdb version from the last converted transaction
575     Swdb swdbObj(swdb, false);
576     auto lastTrans = swdbObj.getLastTransaction();
577 
578     auto trans = swdb_private::Transaction(swdb);
579 
580     // load sequences
581     struct json_object *groups;
582     struct json_object *envs;
583 
584     // add groups
585     if (json_object_object_get_ex(root, "GROUPS", &groups)) {
586         json_object_object_foreach(groups, key, val) {
587             trans.addItem(processGroup (swdb, key, val),
588                           {}, // repoid
589                           TransactionItemAction::INSTALL,
590                           TransactionItemReason::USER);
591         }
592     }
593 
594     // add environments
595     if (json_object_object_get_ex(root, "ENVIRONMENTS", &envs)) {
596         json_object_object_foreach(envs, key, val) {
597             trans.addItem(processEnvironment (swdb, key, val),
598                           {}, // repoid
599                           TransactionItemAction::INSTALL,
600                           TransactionItemReason::USER);
601         }
602     }
603 
604     trans.begin();
605 
606     auto now = time(NULL);
607     trans.setDtBegin(now);
608     trans.setDtEnd(now);
609 
610     if (lastTrans) {
611         trans.setRpmdbVersionBegin(lastTrans->getRpmdbVersionEnd());
612         trans.setRpmdbVersionEnd(trans.getRpmdbVersionBegin());
613     } else {
614         // no transaction found -> use 0 packages + hash for an empty string
615         trans.setRpmdbVersionBegin("0:da39a3ee5e6b4b0d3255bfef95601890afd80709");
616         trans.setRpmdbVersionEnd(trans.getRpmdbVersionBegin());
617     }
618 
619     for (auto i : trans.getItems()) {
620         i->setState(TransactionItemState::DONE);
621         i->save();
622     }
623 
624     trans.finish(TransactionState::DONE);
625 }
626 
627 /**
628  * Load group persistor into JSON object and perform transformation
629  * \param swdb pointer to swdb SQLite3 object
630  */
631 void
transformGroups(SQLite3Ptr swdb)632 Transformer::transformGroups(SQLite3Ptr swdb)
633 {
634     std::string groupsFile(inputDir);
635 
636     // create the groups.json path
637     if (groupsFile.back() != '/') {
638         groupsFile += '/';
639     }
640     groupsFile += "groups.json";
641 
642     std::ifstream groupsStream(groupsFile);
643 
644     if (!groupsStream.is_open()) {
645         return;
646     }
647 
648     std::stringstream buffer;
649     buffer << groupsStream.rdbuf();
650 
651     struct json_object *root = json_tokener_parse(buffer.str().c_str());
652 
653     processGroupPersistor(swdb, root);
654 }
655 
656 /**
657  * Try to find the history database in the inputDir
658  * \return path to the latest history database in the inputDir
659  */
660 std::string
historyPath()661 Transformer::historyPath()
662 {
663     std::string historyDir(inputDir);
664 
665     // construct the history directory path
666     if (historyDir.back() != '/') {
667         historyDir += '/';
668     }
669     historyDir += "history";
670 
671     // vector for possible history DB files
672     std::vector< std::string > possibleFiles;
673 
674     // open history directory
675     struct dirent *dp;
676     std::unique_ptr<DIR, std::function<void(DIR *)>> dirp(opendir(historyDir.c_str()), [](DIR* ptr){
677         closedir(ptr);
678     });
679 
680     if (!dirp) {
681         throw Exception(_("Transformer: can't open history persist dir"));
682     }
683 
684     // iterate over history directory and look for 'history-*.sqlite' files
685     while ((dp = readdir(dirp.get())) != nullptr) {
686         std::string fileName(dp->d_name);
687         if (libdnf::string::startsWith(fileName, "history-") &&
688             libdnf::string::endsWith(fileName, ".sqlite")) {
689             possibleFiles.push_back(fileName);
690         }
691     }
692 
693     if (possibleFiles.empty()) {
694         throw Exception(_("Couldn't find a history database"));
695     }
696 
697     // find the latest DB file
698     std::sort(possibleFiles.begin(), possibleFiles.end());
699 
700     // return the path
701     return historyDir + "/" + possibleFiles.back();
702 }
703 
704 } // namespace libdnf
705