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"