玉兔远程控制 0.1.0-bate6
载入中...
搜索中...
未找到
HistoryDatabase.cpp
1// Author: Kang Lin <kl222@126.com>
2
3#include <QSqlQuery>
4#include <QSqlError>
5#include <QStandardPaths>
6#include <QDir>
7#include <QDebug>
8#include <QLoggingCategory>
9#include <QCoreApplication>
10#include <QJsonDocument>
11#include <QJsonObject>
12#include <QJsonArray>
13#include "HistoryDatabase.h"
14
15static Q_LOGGING_CATEGORY(log, "WebBrowser.History.DB")
16
17#if HAVE_SQLITE
18#include <sqlite3.h> // 需要链接 sqlite3 库
19// SQLite 回调函数
20static void sqlTrace(void* /*data*/, const char* sql) {
21 qDebug(log) << "SQL Trace:" << sql;
22}
23
24// 启用 SQL 跟踪
25void enableSqlTrace(const QString& connectionName = QSqlDatabase::defaultConnection) {
26 QSqlDatabase db = QSqlDatabase::database(connectionName);
27
28 // 获取底层 SQLite 数据库句柄
29 QVariant v = db.driver()->handle();
30 if (v.isValid() && qstrcmp(v.typeName(), "sqlite3*") == 0) {
31 sqlite3* handle = *static_cast<sqlite3**>(v.data());
32 if (handle) {
33 sqlite3_trace(handle, sqlTrace, nullptr);
34 qDebug(log) << "SQLite trace enabled";
35 }
36 }
37}
38#else // HAVE_SQLITE
39void enableSqlTrace(const QString& connectionName)
40{
41}
42#endif // HAVE_SQLITE
43
44CHistoryDatabase* CHistoryDatabase::Instance(const QString &szPath)
45{
46 static CHistoryDatabase* p = nullptr;
47 if(!p) {
48 p = new CHistoryDatabase();
49 if(p) {
50 bool bRet = p->OpenSQLiteDatabase("history_connection", szPath);
51 if(!bRet) {
52 delete p;
53 p = nullptr;
54 }
55 }
56 }
57 return p;
58}
59
60CHistoryDatabase::CHistoryDatabase(QObject *parent)
61 : CDatabase(parent)
62{
63 qDebug(log) << Q_FUNC_INFO;
64}
65
66CHistoryDatabase::~CHistoryDatabase()
67{
68 qDebug(log) << Q_FUNC_INFO;
69}
70
71bool CHistoryDatabase::OnInitializeDatabase()
72{
73 QSqlQuery query(GetDatabase());
74
75 // 创建历史记录表
76 bool success = query.exec(
77 "CREATE TABLE IF NOT EXISTS history ("
78 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
79 " url INTEGER NOT NULL,"
80 " visit_time DATETIME DEFAULT CURRENT_TIMESTAMP"
81 ")"
82 );
83
84 if (!success) {
85 qCritical(log) << "Failed to create table:" << query.lastError().text();
86 return false;
87 }
88
89 // 创建索引
90 query.exec("CREATE INDEX IF NOT EXISTS idx_history_url ON history(url)");
91 query.exec("CREATE INDEX IF NOT EXISTS idx_history_time ON history(visit_time)");
92
93 m_UrlDB.SetDatabase(GetDatabase());
94 m_UrlDB.OnInitializeDatabase();
95 return success;
96}
97
98bool CHistoryDatabase::addHistoryEntry(const QString &url)
99{
100 if (url.isEmpty()) return false;
101
102 int nUrlId = 0;
103 // 检查 URL 是否已存在。如果不存在,则增加
104 nUrlId = m_UrlDB.GetId(url);
105 if(0 == nUrlId) {
106 nUrlId = m_UrlDB.AddUrl(url);
107 if(0 == nUrlId)
108 return false;
109 }
110
111 // 插入新记录
112 QSqlQuery query(GetDatabase());
113 query.prepare(
114 "INSERT INTO history (url, visit_time) "
115 "VALUES (:url, :visit_time)"
116 );
117 query.bindValue(":url", nUrlId);
118 QDateTime tm = QDateTime::currentDateTime();
119 query.bindValue(":visit_time", tm);
120
121 bool success = query.exec();
122 if (!success) {
123 qCritical(log) << "Failed to add history:" << query.lastError().text();
124 }
125
126 return success;
127}
128
129bool CHistoryDatabase::addHistoryEntry(const QString &url, const QString& title, const QDateTime& time)
130{
131 if (url.isEmpty()) return false;
132
133 int nUrlId = 0;
134 // 检查 URL 是否已存在。如果不存在,则增加
135 nUrlId = m_UrlDB.GetId(url);
136 if(0 == nUrlId) {
137 nUrlId = m_UrlDB.AddUrl(url, title);
138 if(0 == nUrlId)
139 return false;
140 }
141
142 // 插入新记录
143 QSqlQuery query(GetDatabase());
144 query.prepare(
145 "INSERT INTO history (url, visit_time) "
146 "VALUES (:url, :visit_time)"
147 );
148 query.bindValue(":url", nUrlId);
149 query.bindValue(":visit_time", time);
150
151 bool success = query.exec();
152 if (!success) {
153 qCritical(log) << "Failed to add history:" << query.lastError().text();
154 }
155
156 return success;
157}
158
159bool CHistoryDatabase::updateHistoryEntry(const QString& url, const QString &title, const QIcon &icon)
160{
161 if (url.isEmpty()) return false;
162
163 int nUrlId = 0;
164 nUrlId = m_UrlDB.GetId(url);
165 if(0 == nUrlId) {
166 nUrlId = m_UrlDB.AddUrl(url, title, icon);
167 return (0 < nUrlId);
168 }
169 return m_UrlDB.UpdateUrl(url, title, icon);
170}
171
172bool CHistoryDatabase::updateHistoryEntry(int id, const QString &title, const QIcon &icon)
173{
174 QSqlQuery query(GetDatabase());
175 query.prepare(
176 "SELECT url"
177 "FROM history "
178 "WHERE id=:id"
179 );
180 query.bindValue(":id", id);
181 if(query.exec() && query.next()) {
182 int urlId = query.value(0).toInt();
183 return m_UrlDB.UpdateUrl(urlId, title, icon);
184 }
185
186 return false;
187}
188
189bool CHistoryDatabase::deleteHistoryEntry(int id)
190{
191 QSqlQuery query(GetDatabase());
192 query.prepare("DELETE FROM history WHERE id = :id");
193 query.bindValue(":id", id);
194
195 return query.exec();
196}
197
198bool CHistoryDatabase::deleteHistoryEntry(const QString &url)
199{
200 int urlId = m_UrlDB.GetId(url);
201 if(0 == urlId)
202 return false;
203 QSqlQuery query(GetDatabase());
204 query.prepare("DELETE FROM history WHERE url = :url");
205 query.bindValue(":url", urlId);
206 return query.exec();
207}
208
209bool CHistoryDatabase::deleteDomainEntries(const QString &szDomain)
210{
211 if(szDomain.isEmpty()) return false;
212 auto domains = m_UrlDB.GetDomain(szDomain);
213 foreach(auto urlId, domains) {
214 QSqlQuery query(GetDatabase());
215 query.prepare("DELETE FROM history WHERE url = :url");
216 query.bindValue(":url", urlId);
217 if(!query.exec())
218 qCritical(log) << "Failed to delete domain:" << szDomain << "url id:" << urlId;
219 m_UrlDB.DeleteUrl(urlId);
220 }
221 return true;
222}
223
224bool CHistoryDatabase::clearHistory(int days)
225{
226 QSqlQuery query(GetDatabase());
227
228 if (days > 0) {
229 QDateTime cutoff = QDateTime::currentDateTime().addDays(-days);
230 query.prepare("DELETE FROM history WHERE visit_time < :cutoff");
231 query.bindValue(":cutoff", cutoff);
232 } else {
233 query.prepare("DELETE FROM history");
234 }
235
236 bool success = query.exec();
237
238 if (success) {
239 // 清理后重置自增ID
240 query.exec("VACUUM");
241 }
242
243 return success;
244}
245
246void CHistoryDatabase::scheduleCleanup(int maxDays, int maxCount)
247{
248 // 按时间清理
249 if (maxDays > 0) {
250 QSqlQuery query(GetDatabase());
251 QDateTime cutoff = QDateTime::currentDateTime().addDays(-maxDays);
252 query.prepare("DELETE FROM history WHERE visit_time < :cutoff");
253 query.bindValue(":cutoff", cutoff);
254 query.exec();
255 }
256
257 // 按数量清理
258 if (maxCount > 0) {
259 QSqlQuery query(GetDatabase());
260 query.prepare(
261 "DELETE FROM history WHERE id IN ("
262 " SELECT id FROM history "
263 " ORDER BY visit_time DESC "
264 " LIMIT -1 OFFSET :maxCount"
265 ")"
266 );
267 query.bindValue(":maxCount", maxCount);
268 query.exec();
269 }
270}
271
272QList<HistoryItem> CHistoryDatabase::getAllHistory(int limit, int offset)
273{
274 QList<HistoryItem> historyList;
275
276 QSqlQuery query(GetDatabase());
277 if(0 > limit) {
278 query.prepare(
279 "SELECT id, url, visit_time "
280 "FROM history "
281 "ORDER BY visit_time DESC "
282 );
283 } else {
284 query.prepare(
285 "SELECT id, url, visit_time "
286 "FROM history "
287 "ORDER BY visit_time DESC "
288 "LIMIT :limit OFFSET :offset"
289 );
290 query.bindValue(":limit", limit);
291 query.bindValue(":offset", offset);
292 }
293 if (query.exec()) {
294 while (query.next()) {
295 HistoryItem item;
296 item.id = query.value(0).toInt();
297 CDatabaseUrl::UrlItem urlItem = m_UrlDB.GetItem(query.value(1).toInt());
298 item.url = urlItem.szUrl;
299 item.title = urlItem.szTitle;
300 item.visitTime = query.value(2).toDateTime();
301 item.icon = urlItem.icon;
302 historyList.append(item);
303 }
304 } else {
305 qCritical(log) << "Failed to get all history:" << query.lastError().text();
306 }
307
308 return historyList;
309}
310
311QList<HistoryItem> CHistoryDatabase::getHistoryByDate(const QDate &date)
312{
313 QList<HistoryItem> historyList;
314
315 QSqlQuery query(GetDatabase());
316 query.prepare(
317 "SELECT id, url, visit_time "
318 "FROM history "
319 "WHERE date(visit_time) = date(:date) "
320 "ORDER BY visit_time DESC"
321 );
322 query.bindValue(":date", date.toString("yyyy-MM-dd"));
323
324 if (query.exec()) {
325 while (query.next()) {
326 HistoryItem item;
327 item.id = query.value(0).toInt();
328 CDatabaseUrl::UrlItem urlItem = m_UrlDB.GetItem(query.value(1).toInt());
329 item.url = urlItem.szUrl;
330 item.title = urlItem.szTitle;
331 item.visitTime = query.value(2).toDateTime();
332 item.icon = urlItem.icon;
333 historyList.append(item);
334 }
335 }
336
337 return historyList;
338}
339
340QList<HistoryItem> CHistoryDatabase::getHistoryByDate(
341 const QDate &start, const QDate &end, int limit)
342{
343 QList<HistoryItem> historyList;
344
345 QSqlQuery query(GetDatabase());
346 query.prepare(
347 "SELECT id, url, visit_time "
348 "FROM history "
349 "WHERE date(visit_time) >= date(:start) AND date(visit_time) <= date(:end) "
350 "ORDER BY visit_time DESC "
351 "LIMIT :limit"
352 );
353 query.bindValue(":start", start.toString("yyyy-MM-dd"));
354 query.bindValue(":end", end.toString("yyyy-MM-dd"));
355 query.bindValue(":limit", limit);
356
357 if (query.exec()) {
358 while (query.next()) {
359 HistoryItem item;
360 item.id = query.value(0).toInt();
361 CDatabaseUrl::UrlItem urlItem = m_UrlDB.GetItem(query.value(1).toInt());
362 item.url = urlItem.szUrl;
363 item.title = urlItem.szTitle;
364 item.visitTime = query.value(2).toDateTime();
365 item.icon = urlItem.icon;
366 historyList.append(item);
367 }
368 }
369
370 return historyList;
371}
372
373QList<HistoryItem> CHistoryDatabase::searchHistory(const QString &keyword)
374{
375 auto items = m_UrlDB.Search(keyword);
376 QList<HistoryItem> retItems;
377 foreach (auto i, items) {
378 HistoryItem item;
379 item.icon = i.icon;
380 item.title = i.szTitle;
381 item.url = i.szUrl;
382 retItems << item;
383 }
384 return retItems;
385}
386
387HistoryItem CHistoryDatabase::getHistoryByUrl(const QString &url)
388{
389 HistoryItem item;
390
391 if(url.isEmpty()) return item;
392 auto urlItem = m_UrlDB.GetItem(url);
393 if(0 >= urlItem.id)
394 return item;
395
396 QSqlQuery query(GetDatabase());
397 query.prepare(
398 "SELECT id, visit_time "
399 "FROM history WHERE url = :url"
400 );
401 query.bindValue(":url", urlItem.id);
402
403 if (query.exec() && query.next()) {
404 item.id = query.value(0).toInt();
405 item.url = urlItem.szUrl;
406 item.title = urlItem.szTitle;
407 item.icon = urlItem.icon;
408 item.visitTime = query.value(1).toDateTime();
409 }
410
411 return item;
412}
413
414HistoryItem CHistoryDatabase::getHistoryById(int id)
415{
416 HistoryItem item;
417
418 QSqlQuery query(GetDatabase());
419 query.prepare(
420 "SELECT id, url, visit_time "
421 "FROM history WHERE id = :id"
422 );
423 query.bindValue(":id", id);
424
425 if (query.exec() && query.next()) {
426 item.id = query.value(0).toInt();
427 int urlId = query.value(1).toInt();
428 auto urlItem = m_UrlDB.GetItem(urlId);
429 item.url = urlItem.szUrl;
430 item.title = urlItem.szTitle;
431 item.icon = urlItem.icon;
432 item.visitTime = query.value(2).toDateTime();
433 }
434
435 return item;
436}
437
438int CHistoryDatabase::getHistoryCount()
439{
440 QSqlQuery query(GetDatabase());
441 query.exec("SELECT COUNT(*) FROM history");
442
443 if (query.next()) {
444 return query.value(0).toInt();
445 }
446
447 return 0;
448}
449
450QDateTime CHistoryDatabase::getLastVisitTime()
451{
452 QSqlQuery query(GetDatabase());
453 query.exec("SELECT MAX(visit_time) FROM history");
454
455 if (query.next()) {
456 return query.value(0).toDateTime();
457 }
458
459 return QDateTime();
460}
461
462bool CHistoryDatabase::importFromJson(const QString &filename)
463{
464 QFile file(filename);
465 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
466 return false;
467
468 do {
469 QJsonDocument doc;
470 doc = QJsonDocument::fromJson(file.readAll());
471 if(!doc.isArray())
472 break;
473 auto array = doc.array();
474 for(auto it = array.begin(); it != array.end(); it++) {
475 auto o = it->toObject();
476 QString url = o["url"].toString();
477 QString title = o["title"].toString();
478 QDateTime time = QDateTime::fromString(o["visit_time"].toString());
479 qDebug(log) << "title:" << title << "url:" << url << "visit_time:" << time;
480 bool ok = addHistoryEntry(url, title, time);
481 if(!ok)
482 qWarning(log) << "Failed to add history:" << o["title"].toString();
483 }
484 } while(0);
485
486 file.close();
487 return true;
488}
489
490bool CHistoryDatabase::exportToJson(const QString &filename)
491{
492 QFile file(filename);
493 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
494 return false;
495
496 QTextStream out(&file);
497#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
498 out.setEncoding(QStringConverter::Utf8);
499#else
500 out.setCodec("UTF-8");
501#endif
502 out.setGenerateByteOrderMark(true); // 添加 UTF-8 BOM
503
504 QJsonDocument doc;
505 QJsonArray list;
506 auto items = getAllHistory();
507 foreach(auto it, items) {
508 QJsonObject title;
509 title.insert("title", it.title);
510 title.insert("url", it.url);
511 title.insert("visit_time", it.visitTime.toString());
512 list.append(title);
513 }
514 doc.setArray(list);
515 out << doc.toJson();
516
517 file.close();
518 return true;
519}
520
521bool CHistoryDatabase::importFromCSV(const QString &filename)
522{
523 QFile file(filename);
524 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
525 qCritical(log) << "Failed to open file" << filename;
526 return false;
527 }
528
529 QTextStream in(&file);
530#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
531 in.setEncoding(QStringConverter::Utf8);
532#else
533 in.setCodec("UTF-8");
534#endif
535 in.setGenerateByteOrderMark(true); // 添加 UTF-8 BOM
536 int importedCount = 0;
537 int lineNumber = 0;
538
539 // 开始事务
540 GetDatabase().transaction();
541
542 try {
543 while (!in.atEnd()) {
544 QString line = in.readLine().trimmed();
545 lineNumber++;
546
547 // 跳过空行和注释
548 if (line.isEmpty() || line.startsWith("#")) {
549 continue;
550 }
551
552 // 跳过表头(第一行)
553 if (lineNumber == 1) {
554 // 验证表头格式
555 if (!validateCsvHeader(line)) {
556 throw QString("Invalid CSV header format");
557 }
558 continue;
559 }
560
561 // 解析 CSV 行
562 QStringList fields = parseCsvLine(line);
563
564 if (fields.size() >= 3) {
565 if (importCsvRecord(fields)) {
566 importedCount++;
567 }
568 } else {
569 qWarning() << "Invalid CSV line" << lineNumber << ":" << line;
570 }
571 }
572
573 file.close();
574
575 if (importedCount == 0) {
576 throw QString("No valid records found in CSV file");
577 }
578
579 if (!GetDatabase().commit()) {
580 throw QString("Failed to commit transaction: %1").arg(GetDatabase().lastError().text());
581 }
582
583 qDebug(log) << "Successfully imported" << importedCount << "records from CSV file";
584 return true;
585 } catch (const QString &error) {
586 GetDatabase().rollback();
587 file.close();
588 qCritical(log) << "CSV import failed at line" << lineNumber << ":" << error;
589 return false;
590 }
591 return false;
592}
593
594bool CHistoryDatabase::exportToCSV(const QString &filename)
595{
596 QFile file(filename);
597 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
598 return false;
599
600 QTextStream out(&file);
601#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
602 out.setEncoding(QStringConverter::Utf8);
603#else
604 out.setCodec("UTF-8");
605#endif
606 out.setGenerateByteOrderMark(true); // 添加 UTF-8 BOM
607
608 // 写入表头
609 const QStringList headers = {
610 "Title", "URL", "Visit Time"
611 };
612 out << headers.join(",") << "\n";
613
614 auto items = getAllHistory();
615 foreach(auto it, items) {
616 QStringList row;
617 row << escapeForCsv(it.title);
618 row << escapeForCsv(it.url);
619 row << it.visitTime.toString();
620 out << row.join(",") << "\n";
621 }
622
623 file.close();
624 return true;
625}
626
627QString CHistoryDatabase::escapeForCsv(const QString &text)
628{
629 if (text.isEmpty())
630 return "\"\"";
631
632 // 判断是否需要引号
633 bool needQuotes = text.contains(',') ||
634 text.contains('"') ||
635 text.contains('\n') ||
636 text.contains('\r') ||
637 text.contains('\t') ||
638 text.startsWith(' ') ||
639 text.endsWith(' ') ||
640 text.startsWith('\t') ||
641 text.endsWith('\t');
642
643 if (!needQuotes)
644 return text;
645
646 // 转义双引号
647 QString escaped = text;
648 escaped.replace("\"", "\"\"");
649
650 return "\"" + escaped + "\"";
651}
652
653QString CHistoryDatabase::unescapeCsvField(const QString &field)
654{
655 if (field.isEmpty()) {
656 return QString();
657 }
658
659 QString unescaped = field;
660
661 // 去除包围的引号
662 if (unescaped.startsWith('"') && unescaped.endsWith('"')) {
663 unescaped = unescaped.mid(1, unescaped.length() - 2);
664 }
665
666 // 反转义双引号
667 unescaped.replace("\"\"", "\"");
668
669 return unescaped.trimmed();
670}
671
672QStringList CHistoryDatabase::parseCsvLine(const QString &line)
673{
674 QStringList fields;
675 QString field;
676 bool inQuotes = false;
677
678 for (int i = 0; i < line.length(); ++i) {
679 QChar ch = line[i];
680
681 if (ch == '"') {
682 // 处理转义的双引号
683 if (i + 1 < line.length() && line[i + 1] == '"') {
684 field += '"';
685 i++; // 跳过下一个引号
686 } else {
687 inQuotes = !inQuotes;
688 }
689 } else if (ch == ',' && !inQuotes) {
690 fields.append(field.trimmed());
691 field.clear();
692 } else {
693 field += ch;
694 }
695 }
696
697 // 添加最后一个字段
698 if (!field.isEmpty()) {
699 fields.append(field.trimmed());
700 }
701
702 return fields;
703}
704
705bool CHistoryDatabase::validateCsvHeader(const QString &headerLine)
706{
707 QStringList headers = parseCsvLine(headerLine);
708
709 // 检查必需的表头
710 if (headers.size() < 3) {
711 return false;
712 }
713
714 // 检查关键字段
715 QStringList requiredHeaders = {"Title", "URL", "Visit Time"};
716 for (const QString &required : requiredHeaders) {
717 if (!headers.contains(required, Qt::CaseInsensitive)) {
718 qWarning(log) << "Missing required header:" << required;
719 return false;
720 }
721 }
722
723 return true;
724}
725
726bool CHistoryDatabase::importCsvRecord(const QStringList &fields)
727{
728 if (fields.size() < 3) {
729 return false;
730 }
731
732 HistoryItem item;
733
734 // 解析字段
735 // 字段顺序:Title, URL, Visit Time
736
737 // Title(可选)
738 item.title = fields[0];
739 // URL(必需)
740 item.url = fields[1];
741
742 // Visit Time(必需)
743 item.visitTime = QDateTime::fromString(fields[2]);
744 if (!item.visitTime.isValid()) {
745 // 尝试其他格式
746 item.visitTime = QDateTime::fromString(fields[2]);
747 if (!item.visitTime.isValid()) {
748 item.visitTime = QDateTime::currentDateTime();
749 }
750 }
751
752 return addHistoryEntry(item.url, item.title, item.visitTime);
753}
754
755bool CHistoryDatabase::ExportToJson(QJsonObject &obj)
756{
757 return true;
758}
759
760bool CHistoryDatabase::ImportFromJson(const QJsonObject &obj)
761{
762 return true;
763}