File indexing completed on 2024-05-19 05:21:54

0001 /*
0002  * Copyright (C) 2003, 2004 by Mark Bucciarelli <mark@hubcapconsutling.com>
0003  * Copyright (C) 2019  Alexander Potashev <aspotashev@gmail.com>
0004  *
0005  *   This program is free software; you can redistribute it and/or modify
0006  *   it under the terms of the GNU General Public License as published by
0007  *   the Free Software Foundation; either version 2 of the License, or
0008  *   (at your option) any later version.
0009  *
0010  *   This program is distributed in the hope that it will be useful,
0011  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
0012  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013  *   GNU General Public License for more details.
0014  *
0015  *   You should have received a copy of the GNU General Public License along
0016  *   with this program; if not, write to the
0017  *      Free Software Foundation, Inc.
0018  *      51 Franklin Street, Fifth Floor
0019  *      Boston, MA  02110-1301  USA.
0020  *
0021  */
0022 
0023 /** TimeTrackerStorage
0024   * This class cares for the storage of ktimetracker's data.
0025   * ktimetracker's data is
0026   * - tasks like "programming for customer foo"
0027   * - events like "from 2009-09-07, 8pm till 10pm programming for customer foo"
0028   * tasks are like the items on your todo list, events are dates when you worked on them.
0029   * ktimetracker's data is stored in a ResourceCalendar object hold by mCalendar.
0030   */
0031 
0032 #include "timetrackerstorage.h"
0033 
0034 #include <QCryptographicHash>
0035 #include <QDateTime>
0036 #include <QDir>
0037 #include <QFileInfo>
0038 #include <QLockFile>
0039 #include <QMultiHash>
0040 #include <QStandardPaths>
0041 
0042 #include <KDirWatch>
0043 #include <KLocalizedString>
0044 
0045 #include "ktt_debug.h"
0046 #include "model/eventsmodel.h"
0047 #include "model/projectmodel.h"
0048 #include "model/task.h"
0049 #include "model/tasksmodel.h"
0050 #include "taskview.h"
0051 #include "widgets/taskswidget.h"
0052 
0053 const QByteArray eventAppName = QByteArray("ktimetracker");
0054 
0055 TimeTrackerStorage::TimeTrackerStorage()
0056     : m_model(nullptr)
0057     , m_taskView(nullptr)
0058 {
0059 }
0060 
0061 // Loads data from filename into view.
0062 QString TimeTrackerStorage::load(TaskView *view, const QUrl &url)
0063 {
0064     if (url.isEmpty()) {
0065         return QStringLiteral("TimeTrackerStorage::load() callled with an empty URL");
0066     }
0067 
0068     // loading might create the file
0069     bool removedFromDirWatch = false;
0070     if (KDirWatch::self()->contains(m_url.toLocalFile())) {
0071         KDirWatch::self()->removeFile(m_url.toLocalFile());
0072         removedFromDirWatch = true;
0073     }
0074 
0075     // If same file, don't reload
0076     if (url == m_url) {
0077         if (removedFromDirWatch) {
0078             KDirWatch::self()->addFile(m_url.toLocalFile());
0079         }
0080         return QString();
0081     }
0082 
0083     if (m_model) {
0084         closeStorage();
0085     }
0086 
0087     m_model = new ProjectModel();
0088 
0089     if (url.isLocalFile()) {
0090         connect(KDirWatch::self(), &KDirWatch::created, this, &TimeTrackerStorage::onFileModified);
0091         connect(KDirWatch::self(), &KDirWatch::dirty, this, &TimeTrackerStorage::onFileModified);
0092         if (!KDirWatch::self()->contains(url.toLocalFile())) {
0093             KDirWatch::self()->addFile(url.toLocalFile());
0094         }
0095     }
0096 
0097     // Create local file resource and add to resources
0098     m_url = url;
0099     FileCalendar m_calendar(m_url);
0100 
0101     m_taskView = view;
0102     m_calendar.reload();
0103 
0104     // Build task view from iCal data
0105     QString err;
0106     eventsModel()->load(m_calendar.rawEvents());
0107     err = loadTasksFromCalendar(m_calendar.rawTodos());
0108 
0109     if (removedFromDirWatch) {
0110         KDirWatch::self()->addFile(m_url.toLocalFile());
0111     }
0112     return err;
0113 }
0114 
0115 QUrl TimeTrackerStorage::fileUrl()
0116 {
0117     return m_url;
0118 }
0119 
0120 QString TimeTrackerStorage::loadTasksFromCalendar(const KCalendarCore::Todo::List &todos)
0121 {
0122     tasksModel()->clear();
0123 
0124     QMultiHash<QString, Task *> map;
0125     for (const auto &todo : todos) {
0126         Task *task = new Task(todo, m_model);
0127         map.insert(todo->uid(), task);
0128         task->invalidateCompletedState();
0129     }
0130 
0131     // 1.1. Load each task under its parent task.
0132     QString err{};
0133     for (const auto &todo : todos) {
0134         Task *task = map.value(todo->uid());
0135         // No relatedTo incident just means this is a top-level task.
0136         if (!todo->relatedTo().isEmpty()) {
0137             Task *newParent = map.value(todo->relatedTo());
0138             // Complete the loading but return a message
0139             if (!newParent) {
0140                 err = i18n("Error loading \"%1\": could not find parent (uid=%2)", task->name(), todo->relatedTo());
0141             } else {
0142                 task->move(newParent);
0143             }
0144         }
0145     }
0146 
0147     return err;
0148 }
0149 
0150 void TimeTrackerStorage::closeStorage()
0151 {
0152     if (m_model) {
0153         delete m_model;
0154         m_model = nullptr;
0155     }
0156 }
0157 
0158 EventsModel *TimeTrackerStorage::eventsModel()
0159 {
0160     if (!m_model) {
0161         qFatal("TimeTrackerStorage::eventsModel is nullptr");
0162     }
0163 
0164     return m_model->eventsModel();
0165 }
0166 
0167 TasksModel *TimeTrackerStorage::tasksModel()
0168 {
0169     if (!m_model) {
0170         qFatal("TimeTrackerStorage::tasksModel is nullptr");
0171     }
0172 
0173     return m_model->tasksModel();
0174 }
0175 
0176 ProjectModel *TimeTrackerStorage::projectModel()
0177 {
0178     if (!m_model) {
0179         qFatal("TimeTrackerStorage::projectModel is nullptr");
0180     }
0181 
0182     return m_model;
0183 }
0184 
0185 bool TimeTrackerStorage::allEventsHaveEndTime(Task *task)
0186 {
0187     for (const auto *event : m_model->eventsModel()->eventsForTask(task)) {
0188         if (!event->hasEndDate()) {
0189             return false;
0190         }
0191     }
0192 
0193     return true;
0194 }
0195 
0196 // static
0197 QString TimeTrackerStorage::createLockFileName(const QUrl &url)
0198 {
0199     QString canonicalSeedString;
0200     QString baseName;
0201     if (url.isLocalFile()) {
0202         QFileInfo fileInfo(url.toLocalFile());
0203         canonicalSeedString = fileInfo.absoluteFilePath();
0204         baseName = fileInfo.fileName();
0205     } else {
0206         canonicalSeedString = url.url();
0207         baseName = QFileInfo(url.path()).completeBaseName();
0208     }
0209 
0210     // Report failure if canonicalSeedString is empty.
0211     if (canonicalSeedString.isEmpty()) {
0212         return QString();
0213     }
0214 
0215     // Remove characters disallowed by operating systems
0216     baseName.replace(QRegularExpression(QStringLiteral("[") + QRegularExpression::escape(QStringLiteral("\\/:*?\"<>|")) + QStringLiteral("]")), QString());
0217 
0218     const QString &hash = QString::fromLatin1(QCryptographicHash::hash(canonicalSeedString.toUtf8(), QCryptographicHash::Sha1).toHex());
0219     const QString &lockBaseName = QStringLiteral("ktimetracker_%1_%2.lock").arg(hash).arg(baseName);
0220 
0221     // Put the lock file in a directory for temporary files
0222     return QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).absoluteFilePath(lockBaseName);
0223 }
0224 
0225 QString TimeTrackerStorage::save()
0226 {
0227     bool removedFromDirWatch = false;
0228     if (KDirWatch::self()->contains(m_url.toLocalFile())) {
0229         KDirWatch::self()->removeFile(m_url.toLocalFile());
0230         removedFromDirWatch = true;
0231     }
0232 
0233     if (!m_model) {
0234         qCWarning(KTT_LOG) << "TimeTrackerStorage::save: m_model is nullptr";
0235         // No i18n() here because it's too technical and unlikely to happen
0236         return QStringLiteral("m_model is nullptr");
0237     }
0238 
0239     const QString &fileLockPath = createLockFileName(m_url);
0240     QLockFile fileLock(fileLockPath);
0241     if (!fileLock.lock()) {
0242         qCWarning(KTT_LOG) << "TimeTrackerStorage::save: m_fileLock->lock() failed";
0243         return i18nc("%1=lock file path", "Could not write lock file \"%1\". Disk full?", fileLockPath);
0244     }
0245 
0246     QString errorMessage{};
0247     std::unique_ptr<FileCalendar> calendar = m_model->asCalendar(m_url);
0248     if (!calendar->save()) {
0249         qCWarning(KTT_LOG) << "TimeTrackerStorage::save: calendar->save() failed";
0250         errorMessage =
0251             i18nc("%1=destination file path/URL", "Failed to save iCalendar file as \"%1\".", m_url.toString());
0252     } else {
0253         qCDebug(KTT_LOG) << "TimeTrackerStorage::save: wrote tasks to" << m_url;
0254     }
0255     fileLock.unlock();
0256 
0257     if (removedFromDirWatch) {
0258         KDirWatch::self()->addFile(m_url.toLocalFile());
0259     }
0260 
0261     return errorMessage;
0262 }
0263 
0264 //----------------------------------------------------------------------------
0265 
0266 bool TimeTrackerStorage::bookTime(const Task *task, const QDateTime &startDateTime, int64_t durationInSeconds)
0267 {
0268     return eventsModel()->bookTime(task, startDateTime, durationInSeconds);
0269 }
0270 
0271 void TimeTrackerStorage::onFileModified()
0272 {
0273     if (!m_model) {
0274         qCWarning(KTT_LOG) << "TaskView::onFileModified(): model is null";
0275         return;
0276     }
0277 
0278     // TODO resolve conflicts if KTimeTracker has unsaved changes in its data structures
0279 
0280     qCDebug(KTT_LOG) << "entering function";
0281 
0282     FileCalendar m_calendar(m_url);
0283     m_calendar.reload();
0284 
0285     // Remember tasks that are running and their start times.
0286     // Maps task UID to task's startTime.
0287     QHash<QString, QDateTime> startTimeForUid{};
0288     for (Task *task : tasksModel()->getActiveTasks()) {
0289         startTimeForUid[task->uid()] = task->startTime();
0290     }
0291 
0292     loadTasksFromCalendar(m_calendar.rawTodos());
0293 
0294     // Restart tasks that have been running, with their start times.
0295     for (Task *task : tasksModel()->getAllTasks()) {
0296         if (startTimeForUid.contains(task->uid())) {
0297             m_taskView->startTimerFor(task, startTimeForUid[task->uid()]);
0298         }
0299     }
0300 
0301     projectModel()->refresh();
0302     m_taskView->tasksWidget()->refresh();
0303 
0304     qCDebug(KTT_LOG) << "exiting onFileModified";
0305 }
0306 
0307 #include "moc_timetrackerstorage.cpp"