mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 00:28:42 +00:00
When exiting, GC complains that the Web Profile is released before Web pages that refer to it with the message: "Release of profile requested but WebEnginePage still not deleted. Expect troubles !". The Web pages are now released explicitly in the destructor to insure that they are freed earlier than the Profile. Fixes #3844
1047 lines
43 KiB
C++
1047 lines
43 KiB
C++
/*
|
|
* Copyright (c) 2013 Mark Liversedge (liversedge@gmail.com)
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation; either version 2 of the License, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
|
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#include "GoldenCheetah.h"
|
|
#include "Settings.h"
|
|
#include "Colors.h"
|
|
#include "GcUpgrade.h"
|
|
#include "Athlete.h"
|
|
#include "VideoWindow.h"
|
|
#include "ErgFile.h"
|
|
#include "RideFile.h"
|
|
#include "JsonRideFile.h"
|
|
#include "Context.h"
|
|
#include "DataProcessor.h"
|
|
#include "MainWindow.h"
|
|
#include "TrainDB.h"
|
|
#include "CloudService.h"
|
|
|
|
#include <QDebug>
|
|
#include <QMessageBox>
|
|
#include <QFileDialog>
|
|
#include <QScrollBar>
|
|
|
|
bool
|
|
GcUpgrade::upgradeConfirmedByUser(const QDir &home)
|
|
{
|
|
|
|
// since the upgrade can fail / multiple times, don't rely on the "lastUpdated" version,
|
|
// but track the "upgrade success" separately in the settings on user level
|
|
bool folderUpgradeSuccess = appsettings->cvalue(home.dirName(), GC_UPGRADE_FOLDER_SUCCESS, false).toBool();
|
|
|
|
// reset upgrade flag - in case flag is set "true" and subfolder "/activities" and /config do NOT exist
|
|
// or - if they exists, but config does not contain any files, then something is wrong with the upgrade flag -
|
|
// (potentially due to a not fitting data restore)
|
|
// so let's reset the flag to "false" and run the upgrade again - this can never go wrong (if their is
|
|
// nothing to be upgrade - upgrade a success anyway
|
|
|
|
AthleteDirectoryStructure newHome(home);
|
|
if (folderUpgradeSuccess && !newHome.upgradedDirectoriesHaveData()) {
|
|
folderUpgradeSuccess = false;
|
|
appsettings->setCValue(home.dirName(), GC_UPGRADE_FOLDER_SUCCESS, false);
|
|
}
|
|
|
|
if (!folderUpgradeSuccess) {
|
|
|
|
GcUpgradeExecuteDialog msgBox(home);
|
|
if (msgBox.exec() == QDialog::Accepted) return true;
|
|
|
|
// if not accepted
|
|
return false;
|
|
|
|
}
|
|
|
|
return true; // if there is no upgrade needed, just proceed
|
|
}
|
|
|
|
int
|
|
GcUpgrade::upgrade(const QDir &home)
|
|
{
|
|
|
|
// what was the last version? -- do we need to upgrade?
|
|
int last = appsettings->cvalue(home.dirName(), GC_VERSION_USED, 0).toInt();
|
|
|
|
// Upgrade processing was introduced in Version 3 -- below must be performed
|
|
// for athlete directories from prior to Version 3
|
|
// and can essentially be used as a template for all major release
|
|
// upgrades as it deletes old stuff and sets clean
|
|
|
|
//----------------------------------------------------------------------
|
|
// 3.0 upgrade processing
|
|
//----------------------------------------------------------------------
|
|
if (!last || last < VERSION3_BUILD) {
|
|
|
|
// For now we always do the same thing
|
|
// when we have some maturity with versions we may
|
|
// choose to do different things
|
|
if (last < VERSION3_BUILD) {
|
|
|
|
// 1. Delete old files
|
|
QStringList oldfiles;
|
|
oldfiles << "*.cpi";
|
|
oldfiles << "*.bak";
|
|
foreach (QString oldfile, home.entryList(oldfiles, QDir::Files)) {
|
|
QFile old(QString("%1/%2").arg(home.canonicalPath()).arg(oldfile));
|
|
old.remove();
|
|
|
|
}
|
|
|
|
// 2. Remove old CLucece 'index'
|
|
QFile index(QString("%1/index").arg(home.canonicalPath()));
|
|
if (index.exists()) {
|
|
removeIndex(index);
|
|
}
|
|
|
|
// 3. Remove metricDBv3 - force rebuild including the search index
|
|
QFile db(QString("%1/metricDBv3").arg(home.canonicalPath()));
|
|
if (db.exists()) db.remove();
|
|
|
|
// 4. Set default weight to 75kg if currently zero
|
|
double weight_ = appsettings->cvalue(home.dirName(), GC_WEIGHT, "75.0").toString().toDouble();
|
|
if (weight_ <= 0.00) appsettings->setCValue(home.dirName(), GC_WEIGHT, "75.0");
|
|
|
|
// 5. startup with common sidebars shown (less ugly)
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/LTM/hide"), true);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/LTM/hide/0"), false);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/LTM/hide/1"), false);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/LTM/hide/2"), false);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/LTM/hide/3"), true);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/analysis/hide"), true);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/analysis/hide/0"), false);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/analysis/hide/1"), true);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/analysis/hide/2"), false);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/analysis/hide/3"), true);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/diary/hide"), true);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/diary/hide/0"), false);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/diary/hide/1"), false);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/diary/hide/2"), true);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/train/hide"), true);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/train/hide/0"), false);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/train/hide/1"), false);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/train/hide/2"), false);
|
|
appsettings->setCValue(home.dirName(), GC_QSETTINGS_ATHLETE_LAYOUT+QString("splitter/train/hide/3"), false);
|
|
|
|
// 6. Delete any old measures.xml -- its for withings only
|
|
QFile msxml(QString("%1/measures.xml").arg(home.canonicalPath()));
|
|
if (msxml.exists()) msxml.remove();
|
|
|
|
|
|
}
|
|
}
|
|
|
|
// Versions after 3 should add their upgrade processing at this point
|
|
// DO NOT CHANGE THE VERSION 3 UPGRADE PROCESS ABOVE, ADD TO IT BELOW
|
|
|
|
//----------------------------------------------------------------------
|
|
// 3.0 SP2 upgrade processing
|
|
//----------------------------------------------------------------------
|
|
if (last < VERSION3_SP2) {
|
|
|
|
// 2. Remove old CLucece 'index'
|
|
QFile index(QString("%1/index").arg(home.canonicalPath()));
|
|
if (index.exists()) {
|
|
removeIndex(index);
|
|
}
|
|
|
|
// 3. Remove metricDBv3 - force rebuild including the search index
|
|
QFile db(QString("%1/metricDBv3").arg(home.canonicalPath()));
|
|
if (db.exists()) db.remove();
|
|
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// 3.1 upgrade processing
|
|
//----------------------------------------------------------------------
|
|
|
|
if (last < VERSION31_BUILD) {
|
|
|
|
// We sought to reset the user defaults in v3.1 to
|
|
// move away from the ugly default used since GC first
|
|
// released. This is the first time we actively applied
|
|
// a new theme and color setting for users.
|
|
|
|
// For a full breakdown of all activities applied in VERSION 3.1
|
|
// they are listed in detail on the associated gitub issue:
|
|
// see https://github.com/GoldenCheetah/GoldenCheetah/issues/883
|
|
|
|
// 1. Delete all backup, CPX, Metrics and Lucene Index
|
|
QStringList oldfiles;
|
|
oldfiles << "*.cpi";
|
|
oldfiles << "*.bak";
|
|
foreach (QString oldfile, home.entryList(oldfiles, QDir::Files)) {
|
|
QFile old(QString("%1/%2").arg(home.canonicalPath()).arg(oldfile));
|
|
old.remove();
|
|
}
|
|
|
|
QFile index(QString("%1/index").arg(home.canonicalPath()));
|
|
if (index.exists()) {
|
|
removeIndex(index);
|
|
}
|
|
|
|
QFile db(QString("%1/metricDBv3").arg(home.canonicalPath()));
|
|
if (db.exists()) db.remove();
|
|
|
|
|
|
// 2. Remove any old charts.xml (it will be WRONG!)
|
|
QFile charts(QString("%1/charts.xml").arg(home.canonicalPath()));
|
|
if (charts.exists()) charts.remove();
|
|
|
|
// 3. Reset colour defaults **
|
|
GCColor::applyTheme(0); // set to default theme
|
|
|
|
// 4. Theme and Chrome Color
|
|
QString theme = "Flat";
|
|
QColor chromeColor = QColor(0xec,0xec,0xec);
|
|
#ifdef Q_OS_MAC
|
|
// Yosemite or earlier
|
|
if (QSysInfo::MacintoshVersion >= 12) {
|
|
|
|
chromeColor = QColor(235,235,235);
|
|
} else {
|
|
|
|
// prior to Yosemite .. metallic
|
|
theme = "Mac";
|
|
chromeColor = QColor(215,215,215);
|
|
}
|
|
#endif
|
|
QString colorstring = QString("%1:%2:%3").arg(chromeColor.red())
|
|
.arg(chromeColor.green())
|
|
.arg(chromeColor.blue());
|
|
appsettings->setValue("CCHROME", colorstring);
|
|
GCColor::setColor(CCHROME, chromeColor);
|
|
|
|
// 5. Metrics and Notes keywords
|
|
QString filename = home.canonicalPath()+"/metadata.xml";
|
|
if (QFile(filename).exists()) {
|
|
|
|
QList<KeywordDefinition> keywordDefinitions;
|
|
QList<FieldDefinition> fieldDefinitions;
|
|
QString colorfield;
|
|
QList<DefaultDefinition> defaultDefinitions;
|
|
|
|
// read em in
|
|
RideMetadata::readXML(filename, keywordDefinitions, fieldDefinitions, colorfield, defaultDefinitions);
|
|
|
|
bool updated=false;
|
|
|
|
//
|
|
// ADD METRICS TO METADATA TAB
|
|
//
|
|
int pos = -1;
|
|
int indexTSS=-1, indexAnTISS=-1, indexAeTISS=-1;
|
|
for(int i=0; i < fieldDefinitions.count(); i++) {
|
|
|
|
// current ...
|
|
FieldDefinition f = fieldDefinitions[i];
|
|
|
|
if (f.tab == tr("Metric") && pos < 0) pos = i;
|
|
if (f.name == "TSS") indexTSS=i;
|
|
if (f.name == tr("Aerobic TISS")) indexAeTISS=i;
|
|
if (f.name == tr("Anaerobic TISS")) indexAnTISS=i;
|
|
}
|
|
|
|
// ok, we need to add them to the metadata
|
|
if (indexTSS < 0 || indexAnTISS < 0 || indexAeTISS < 0) {
|
|
|
|
// lets add all at the same place
|
|
if (indexTSS >= 0) pos = indexTSS;
|
|
else if (indexAnTISS >= 0) pos = indexAnTISS;
|
|
else if (indexAeTISS >= 0) pos = indexAeTISS;
|
|
|
|
// ok, one at a time, using this as a template
|
|
FieldDefinition add;
|
|
add.tab = pos >= 0 ? fieldDefinitions[pos].tab : tr("Metric");
|
|
add.diary = false;
|
|
add.type = 4; // double
|
|
|
|
// now set pos to non-negative if needed
|
|
if (pos < 0) pos = 1;
|
|
|
|
// add them
|
|
if (indexAnTISS < 0) {
|
|
add.name = tr("Anaerobic TISS");
|
|
fieldDefinitions.insert(pos, add);
|
|
}
|
|
if (indexAeTISS < 0) {
|
|
add.name = tr("Aerobic TISS");
|
|
fieldDefinitions.insert(pos, add);
|
|
}
|
|
if (indexTSS < 0) {
|
|
add.name = tr("TSS");
|
|
fieldDefinitions.insert(pos, add);
|
|
}
|
|
updated = true;
|
|
}
|
|
|
|
//
|
|
// DEPRECATE 'default' color keyword and if needed
|
|
// ADD 'Reverse' color keyword
|
|
//
|
|
int defaultIndex = -1, reverseIndex = -1;
|
|
for(int i=0; i<keywordDefinitions.count(); i++) {
|
|
if (keywordDefinitions[i].name == "Default") defaultIndex = i;
|
|
if (keywordDefinitions[i].name == "Reverse") reverseIndex = i;
|
|
}
|
|
|
|
// no more default
|
|
if (defaultIndex >= 0) {
|
|
updated = true;
|
|
keywordDefinitions.removeAt(defaultIndex);
|
|
}
|
|
|
|
// no reverse ?
|
|
if (reverseIndex < 0) {
|
|
updated = true;
|
|
KeywordDefinition add;
|
|
add.name = "Reverse";
|
|
add.color = QColor(Qt::black);
|
|
keywordDefinitions << add;
|
|
}
|
|
|
|
if (updated) {
|
|
// write a new updated version
|
|
RideMetadata::serialize(filename, keywordDefinitions, fieldDefinitions, colorfield, defaultDefinitions);
|
|
}
|
|
}
|
|
|
|
// ** NOTE:
|
|
// ** Suggestions to update CP/W'/Zones have been ignored due to the
|
|
// ** high risk of breaking user setups -- this is due to the complexity
|
|
// ** and multiple ways the user can manage their zones.
|
|
|
|
// BELOW ARE PROBLEMATIC TOO:
|
|
// ** there are no functions to read/write the layout.xml
|
|
// ** files without refactoring HomeWindow to do so -- which
|
|
// ** is a risky change and instead we will need the user
|
|
// ** to reset their layout to get the latest chart setup:
|
|
// Add a W'bal chart to the ride view
|
|
// Add a CP History chart to the trend view
|
|
// Add a Library chart to the trend view
|
|
|
|
// PM deprecation has been handled by returning an LTM chart with
|
|
// PMC curves when an PM chart is still in the layout.
|
|
|
|
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// 3.2 (formerly 3.11) upgrade processing
|
|
//----------------------------------------------------------------------
|
|
|
|
if (last < VERSION311_BUILD) {
|
|
|
|
// add here the standard upgrade tasks
|
|
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
// 3.3 upgrade processing
|
|
//----------------------------------------------------------------------
|
|
|
|
if (last < VERSION33_BUILD) {
|
|
|
|
trainDB->upgradeDefaultEntriesWorkout();
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
// 3.5 upgrade processing
|
|
//----------------------------------------------------------------------
|
|
if (last < VERSION35_BUILD) {
|
|
|
|
// metallic style deprecated
|
|
appsettings->setValue(GC_CHROME, "Flat");
|
|
|
|
// set the default scale factor to 1.0, if not already done
|
|
if (appsettings->value(NULL, GC_FONT_SCALE, "0").toDouble() == 0)
|
|
appsettings->setValue(GC_FONT_SCALE, QVariant::fromValue(1.0f));
|
|
|
|
// cloud services now need a setting to say if active or not
|
|
CloudServiceFactory::instance().upgrade(home.dirName());
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
// ... here any further Release Number dependent Upgrade Process is to be added ...
|
|
//----------------------------------------------------------------------
|
|
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
// All Version dependent Upgrade Steps are done ...
|
|
//----------------------------------------------------------------------
|
|
|
|
// FINALLY -- Set latest version - so only tries to upgrade once
|
|
appsettings->setCValue(home.dirName(), GC_VERSION_USED, VERSION_LATEST);
|
|
|
|
//----------------------------------------------------------------------
|
|
// 3.2 new subfolder introduction and upgrade processing
|
|
//----------------------------------------------------------------------
|
|
|
|
|
|
// now the special "folder structure" upgrade - which is tracked separately on success
|
|
|
|
bool folderUpgradeSuccess = appsettings->cvalue(home.dirName(), GC_UPGRADE_FOLDER_SUCCESS, false).toBool();
|
|
|
|
// now let's check if upgrade is necessary and do the job
|
|
if (!folderUpgradeSuccess) {
|
|
|
|
// initials logs,...
|
|
errorCount = 0;
|
|
upgradeLog = new GcUpgradeLogDialog(home);
|
|
upgradeLog->show();
|
|
|
|
// 1. create the new subDirs structure
|
|
upgradeLog->append(tr("Start creating of: Directories... "),3);
|
|
|
|
AthleteDirectoryStructure newHome(home);
|
|
newHome.createAllSubdirs();
|
|
//now we can created the QSettings for the Athlete and Migrate the oldseetings
|
|
appsettings->initializeQSettingsAthlete(gcroot, home.dirName());
|
|
|
|
if (!newHome.subDirsExist()) {
|
|
upgradeLog->append(QString(tr("Error: Creation of subdirectories failed")),2);
|
|
errorCount++;
|
|
} else {
|
|
upgradeLog->append(QString(tr("Creation of subdirectories successful")),2);
|
|
}
|
|
|
|
// 2. Delete all backup, CPX, Metrics and Lucene Index
|
|
QStringList oldfiles;
|
|
oldfiles << "*.cpi";
|
|
oldfiles << "*.cpx";
|
|
foreach (QString oldfile, home.entryList(oldfiles, QDir::Files)) {
|
|
QFile old(QString("%1/%2").arg(home.canonicalPath()).arg(oldfile));
|
|
old.remove();
|
|
}
|
|
|
|
QFile index(QString("%1/index").arg(home.canonicalPath()));
|
|
if (index.exists()) {
|
|
removeIndex(index);
|
|
}
|
|
|
|
QFile db(QString("%1/metricDBv3").arg(home.canonicalPath()));
|
|
if (db.exists()) db.remove();
|
|
|
|
//3. move the existing files to the new SubDirs
|
|
//3.1 move config
|
|
|
|
QStringList configFiles;
|
|
configFiles << "*.xml";
|
|
configFiles << "*.zones";
|
|
configFiles << "avatar.*";
|
|
upgradeLog->append(tr("Start copying of: Configuration files... "),3);
|
|
|
|
int ok = 0; int fail = 0;
|
|
foreach (QString configFile, newHome.root().entryList(configFiles, QDir::Files)) {
|
|
bool success = moveFile(QString("%1/%2").arg(newHome.root().canonicalPath()).arg(configFile),
|
|
QString("%1/%2").arg(newHome.config().canonicalPath()).arg(configFile));
|
|
if (success) {
|
|
ok++;
|
|
} else {
|
|
fail++;
|
|
}
|
|
}
|
|
errorCount += fail;
|
|
upgradeLog->append(QString(tr("%1 configuration files moved to subdirectory: %2 - %3 failed" ))
|
|
.arg(QString::number(ok)).arg(newHome.config().dirName()).arg(QString::number(fail)),2);
|
|
|
|
// 3.2 move the calendar
|
|
QStringList calFiles;
|
|
calFiles << "*.ics";
|
|
upgradeLog->append(tr("Start copying of: Calendar files..."),3);
|
|
|
|
ok = 0; fail = 0;
|
|
foreach (QString calFile, newHome.root().entryList(calFiles, QDir::Files)) {
|
|
bool success = moveFile(QString("%1/%2").arg(newHome.root().canonicalPath()).arg(calFile),
|
|
QString("%1/%2").arg(newHome.calendar().canonicalPath()).arg(calFile));
|
|
if (success) {
|
|
ok++;
|
|
} else {
|
|
fail++;
|
|
}
|
|
}
|
|
errorCount += fail;
|
|
upgradeLog->append(QString((tr("%1 calendar files moved to subdirectory: %2 - %3 failed"))
|
|
.arg(QString::number(ok)).arg(newHome.calendar().dirName()).arg(QString::number(fail))),2);
|
|
|
|
// 3.3 move the logs
|
|
QStringList logFiles;
|
|
logFiles << "*.log";
|
|
upgradeLog->append(tr("Start copying of: Log files..."),3);
|
|
ok = 0; fail = 0;
|
|
foreach (QString logFile, newHome.root().entryList(logFiles, QDir::Files)) {
|
|
bool success = moveFile(QString("%1/%2").arg(newHome.root().canonicalPath()).arg(logFile),
|
|
QString("%1/%2").arg(newHome.logs().canonicalPath()).arg(logFile));
|
|
if (success) {
|
|
ok++;
|
|
} else {
|
|
fail++;
|
|
|
|
}
|
|
}
|
|
errorCount += fail;
|
|
upgradeLog->append(QString((tr("%1 log files moved to subdirectory: %2 - %3 failed"))
|
|
.arg(QString::number(ok)).arg(newHome.logs().dirName()).arg(QString::number(fail))),2);
|
|
|
|
|
|
// 3.4 move the .JSON and .GC first
|
|
QStringList jsonFiles;
|
|
jsonFiles << "*.json";
|
|
jsonFiles << "*.gc";
|
|
upgradeLog->append(tr("Start copying of: Activity files (.JSON / .GC)..."),3);
|
|
ok = 0; fail = 0;
|
|
foreach (QString jsonFile, newHome.root().entryList(jsonFiles, QDir::Files)) {
|
|
bool success = moveFile(QString("%1/%2").arg(newHome.root().canonicalPath()).arg(jsonFile),
|
|
QString("%1/%2").arg(newHome.activities().canonicalPath()).arg(jsonFile));
|
|
if (success) {
|
|
ok++;
|
|
} else {
|
|
fail++;
|
|
}
|
|
}
|
|
errorCount += fail;
|
|
upgradeLog->append(QString(tr("%1 activity (.JSON, .GC) files moved to subdirectory: %2 - %3 failed" ))
|
|
.arg(QString::number(ok)).arg(newHome.activities().dirName()).arg(QString::number(fail)),2);
|
|
|
|
|
|
// 3.6 keep the .BAK files and store in /fileBackup()
|
|
QStringList bakFiles;
|
|
bakFiles << "*.bak";
|
|
upgradeLog->append(tr("Start copying of: Activity files (.BAK)..."),3);
|
|
ok = 0; fail = 0;
|
|
foreach (QString bakFile, newHome.root().entryList(bakFiles, QDir::Files)) {
|
|
bool success = moveFile(QString("%1/%2").arg(newHome.root().canonicalPath()).arg(bakFile),
|
|
QString("%1/%2").arg(newHome.fileBackup().canonicalPath()).arg(bakFile));
|
|
if (success) {
|
|
ok++;
|
|
} else {
|
|
fail++;
|
|
}
|
|
}
|
|
errorCount += fail;
|
|
upgradeLog->append(QString(tr("%1 activity backup (.BAK) files moved to subdirectory: %2 - %3 failed" ))
|
|
.arg(QString::number(ok)).arg(newHome.fileBackup().dirName()).arg(QString::number(fail)),2);
|
|
|
|
// 3.6 now sort the rest of the files (extension checks are re-use)
|
|
MediaHelper mediaFile;
|
|
ok = 0; fail = 0;
|
|
upgradeLog->append(tr("Start copying of: Media and Workout files... "),3);
|
|
foreach (QString otherFile, newHome.root().entryList(QDir::Files)) {
|
|
// check for workout and media files
|
|
if (mediaFile.isMedia(otherFile) || ErgFile::isWorkout(otherFile)) {
|
|
QDir r;
|
|
bool success = moveFile(QString("%1/%2").arg(newHome.root().canonicalPath()).arg(otherFile),
|
|
QString("%1/%2").arg(newHome.workouts().canonicalPath()).arg(otherFile));
|
|
if (success) {
|
|
ok++;
|
|
} else {
|
|
fail++;
|
|
}
|
|
}
|
|
}
|
|
errorCount += fail;
|
|
upgradeLog->append(QString(tr("%1 media and workout files moved to subdirectory: \\%2 - %3 failed"))
|
|
.arg(QString::number(ok)).arg(newHome.workouts().dirName()).arg(QString::number(fail)),2);
|
|
|
|
// the conversion of all activities to .json is done in "lateUpgrade" - since the prerequisites
|
|
// on the "context" setup are not fulfilled at this early stage
|
|
|
|
|
|
}
|
|
|
|
// other 3.2 upgrade tasks, mostly cosmetic
|
|
if (last < VERSION32_BUILD) {
|
|
|
|
// trend plot matches ride plot, as newly introduced
|
|
// just do for first time we run 3.2 and set to ride plot
|
|
QColor color = GCColor::getColor(CRIDEPLOTBACKGROUND);
|
|
GCColor::setColor(CTRENDPLOTBACKGROUND, color);
|
|
|
|
// and update config
|
|
QString colorstring = QString("%1:%2:%3").arg(color.red())
|
|
.arg(color.green())
|
|
.arg(color.blue());
|
|
appsettings->setValue("COLORTRENDPLOTBACKGROUND", colorstring);
|
|
|
|
// and on non-Mac platforms we want flat look and feel
|
|
// by default now, the metal look is de-rigeur
|
|
#ifndef Q_OS_MAC
|
|
color = QColor(0xe5,0xe5,0xe5);
|
|
colorstring = QString("%1:%2:%3").arg(color.red())
|
|
.arg(color.green())
|
|
.arg(color.blue());
|
|
appsettings->setValue("CCHROME", colorstring);
|
|
appsettings->setValue(GC_CHROME, "Flat");
|
|
#endif
|
|
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
GcUpgrade::upgradeLate(Context *context)
|
|
{
|
|
// check the special "folder structure" upgrade - which is tracked separately on success
|
|
bool folderUpgradeSuccess = appsettings->cvalue(context->athlete->home->root().dirName(), GC_UPGRADE_FOLDER_SUCCESS, false).toBool();
|
|
if (!folderUpgradeSuccess) {
|
|
|
|
// switch off automatic Fix tools (
|
|
DataProcessorFactory::instance().setAutoProcessRule(false);
|
|
|
|
// 1. convert the rest of activities to .json and move the converted ones to /imports
|
|
// prepare the suffixes for check of the files
|
|
QStringList suffixList = RideFileFactory::instance().suffixes();
|
|
QRegExp suffixes(QString("^(%1)$").arg(suffixList.join("|")));
|
|
suffixes.setCaseSensitivity(Qt::CaseInsensitive);
|
|
|
|
int ok = 0; int fail = 0; int okConvert = 0; int failConvert = 0;
|
|
upgradeLog->append(tr("Start conversion of native activity files to GoldenCheetah .JSON format..."),3 );
|
|
|
|
// GC file Name format
|
|
QRegExp rx ("^((\\d\\d\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d)_(\\d\\d))\\.(.+)$");
|
|
bool fileNameValid;
|
|
|
|
foreach (QString activitiesFileName, context->athlete->home->root().entryList(QDir::Files)) {
|
|
|
|
QString fullFileName = context->athlete->home->root().canonicalPath() + "/" + activitiesFileName;
|
|
QFileInfo fullFileInfo(fullFileName);
|
|
|
|
if (fullFileInfo.exists() && fullFileInfo.isFile() && fullFileInfo.isReadable()) {
|
|
|
|
if (suffixes.exactMatch(fullFileInfo.suffix())) {
|
|
// We have an activity file which is NOT JSON or GC (since they have been moved already) - let's convert
|
|
QStringList errors;
|
|
QFile currentFile(fullFileName);
|
|
|
|
// Check if the filename is formattted according the GC filename format (since this format is used to determine
|
|
// the ride start date and time
|
|
fileNameValid = true;
|
|
if (!rx.exactMatch(fullFileInfo.fileName())) {
|
|
fileNameValid = false;
|
|
} else {
|
|
// format is fine, check if the file name really contains a valid date/time info
|
|
QDate date(rx.cap(2).toInt(), rx.cap(3).toInt(),rx.cap(4).toInt());
|
|
QTime time(rx.cap(5).toInt(), rx.cap(6).toInt(),rx.cap(7).toInt());
|
|
|
|
if (!(date.isValid() && time.isValid())) {
|
|
fileNameValid = false;
|
|
}
|
|
}
|
|
|
|
// only process if the file name contains a valid date/time - otherwise user needs to check
|
|
if (fileNameValid) {
|
|
|
|
RideFile *ride = RideFileFactory::instance().openRideFile(context, currentFile, errors);
|
|
|
|
// did it parse ok ? (all files here were alrady parsed when importing)
|
|
if (ride) {
|
|
|
|
// serialize
|
|
QString targetFileName = activitiesFileName;
|
|
int dot = targetFileName.lastIndexOf(".");
|
|
assert(dot >= 0);
|
|
targetFileName.truncate(dot);
|
|
targetFileName.append(".json");
|
|
// add Source File Tag + New File Name
|
|
ride->setTag("Source Filename", activitiesFileName);
|
|
ride->setTag("Filename", targetFileName);
|
|
JsonFileReader reader;
|
|
QFile target(context->athlete->home->activities().canonicalPath() + "/" + targetFileName);
|
|
reader.writeRideFile(context, ride, target);
|
|
okConvert++;
|
|
upgradeLog->append(tr("-> Information: Activity %1 - Successfully converted to .JSON").arg(activitiesFileName));
|
|
|
|
// copy source file to the /imports folder (only if conversion was successful)
|
|
bool success = moveFile(QString("%1/%2").arg(context->athlete->home->root().canonicalPath()).arg(activitiesFileName),
|
|
QString("%1/%2").arg(context->athlete->home->imports().canonicalPath()).arg(activitiesFileName));
|
|
if (success) {
|
|
ok++;
|
|
} else {
|
|
fail++;
|
|
}
|
|
} else {
|
|
failConvert++;
|
|
upgradeLog->append(tr("-> Error: Activity %1 - Conversion errors: ").arg(activitiesFileName),2);
|
|
foreach (QString error, errors) {
|
|
upgradeLog->append(tr("......... message(s) of .JSON conversion): ") + error);
|
|
upgradeLog->append("<br>");
|
|
}
|
|
}
|
|
|
|
// clear
|
|
delete ride;
|
|
|
|
} else {
|
|
failConvert++;
|
|
upgradeLog->append(tr("-> Error: Activity %1 - Invalid File Name (expected format 'YYYY_MM_DD_HH_MM_SS.%2')").arg(activitiesFileName).arg(fullFileInfo.suffix()),2);
|
|
}
|
|
}
|
|
|
|
} else {
|
|
failConvert++;
|
|
upgradeLog->append(tr("-> Error: Activity %1 - Problem reading file").arg(activitiesFileName),2);
|
|
}
|
|
}
|
|
|
|
errorCount+=fail;
|
|
errorCount+=failConvert;
|
|
|
|
upgradeLog->append(QString(tr("%1 activity files converted to .JSON and stored in subdirectory: %2 - %3 failed" ))
|
|
.arg(QString::number(okConvert)).arg(context->athlete->home->activities().dirName()).arg(QString::number(failConvert)),2);
|
|
|
|
upgradeLog->append(QString(tr("%1 converted activity source files moved to subdirectory: %2 - %3 failed" ))
|
|
.arg(QString::number(ok)).arg(context->athlete->home->imports().dirName()).arg(QString::number(fail)),2);
|
|
|
|
// Total Number of errors
|
|
if (errorCount == 0) {
|
|
upgradeLog->append(QString(tr("Summary: No errors detected - upgrade successful" )),3);
|
|
appsettings->setCValue(context->athlete->home->root().dirName(), GC_UPGRADE_FOLDER_SUCCESS, true);
|
|
} else {
|
|
upgradeLog->append(QString(tr("Summary: %1 errors detected - please check log details before proceeding" ))
|
|
.arg(QString::number(errorCount)),3);
|
|
|
|
upgradeLog->append(QString(tr("<center><br>After choosing 'Proceed to Athlete', the system will open the athlete window using the "
|
|
"converted data. Depending on the errors this might lead to follow-up errors and incomplete "
|
|
"athlete data. You may either fix the error(s) in your directory directly, or go back "
|
|
"to your last backup and correct the error(s) in the source data. <br>The upgrade process "
|
|
"will be done again each time you open the athlete, until the conversion was "
|
|
"successful - and had no more errors.</center>")),2);
|
|
|
|
upgradeLog->append(QString(tr("<center><br>Latest information about possible upgrade problems and concepts to resolve them are available in the<br>"
|
|
"<a href= \"https://github.com/GoldenCheetah/GoldenCheetah/wiki/Upgrade_v3.2_Troubleshooting_Guide\" target=\"_blank\">"
|
|
"Upgrade v3.2 Troubleshooting Guide<a>")),1);
|
|
|
|
// document that upgrade failed at least one time
|
|
appsettings->setCValue(context->athlete->home->root().dirName(), GC_UPGRADE_FOLDER_SUCCESS, false);
|
|
|
|
}
|
|
|
|
// switch automatic Fix tools on again
|
|
DataProcessorFactory::instance().setAutoProcessRule(true);
|
|
|
|
// show upgrade log
|
|
upgradeLog->enableButtons();
|
|
upgradeLog->exec();
|
|
|
|
// user can only select "Accept" to end with the upgrade step
|
|
return 0;
|
|
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool
|
|
GcUpgrade::moveFile(const QString &source, const QString &target) {
|
|
|
|
QFile r(source);
|
|
|
|
// first try it with a rename
|
|
if (r.rename(target)) return true; // job is done
|
|
|
|
// now the harder variant (copy & delete)
|
|
if (r.copy(target))
|
|
{
|
|
// try to remove - but if this fails, no problem, file has been copied at least
|
|
if (!r.remove()) {
|
|
// log, even though it's not very critical it needs to be cleaned up - so report as error
|
|
upgradeLog->append(QString(tr("-> Error: Deletion of copied file '%1' failed" )).arg(source),2);
|
|
return false;
|
|
} else {
|
|
// copy & remove worked fine - we are done
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// copying failed - so give up and report
|
|
upgradeLog->append((tr("-> Error moving file : ") + source),2);
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
class FileUtil
|
|
{
|
|
public:
|
|
static bool removeDir(const QString &dirName) {
|
|
|
|
bool result = true;
|
|
QDir dir(dirName);
|
|
|
|
if (dir.exists(dirName)) {
|
|
foreach(QFileInfo info, dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)) {
|
|
if (info.isDir()) {
|
|
result = FileUtil::removeDir(info.absoluteFilePath());
|
|
|
|
} else {
|
|
|
|
result = QFile::remove(info.absoluteFilePath());
|
|
}
|
|
|
|
if (!result) { return result; }
|
|
|
|
}
|
|
result = dir.rmdir(dirName);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
};
|
|
|
|
|
|
void
|
|
GcUpgrade::removeIndex(QFile &index)
|
|
{
|
|
FileUtil::removeDir(index.fileName());
|
|
}
|
|
|
|
|
|
GcUpgradeExecuteDialog::GcUpgradeExecuteDialog(QDir athleteHomeDir) : QDialog(NULL, Qt::Dialog)
|
|
{
|
|
|
|
const QString athlete = athleteHomeDir.dirName();
|
|
|
|
setWindowTitle(QString(tr("Athlete %1").arg(athlete)));
|
|
this->setMinimumWidth(550*dpiXFactor);
|
|
|
|
QVBoxLayout *layout = new QVBoxLayout(this);
|
|
|
|
QHBoxLayout *toprow = new QHBoxLayout;
|
|
|
|
QPushButton *critical = new QPushButton(style()->standardIcon(QStyle::SP_MessageBoxWarning), "", this);
|
|
critical->setFixedSize(80*dpiXFactor,80*dpiYFactor);
|
|
critical->setFlat(true);
|
|
critical->setIconSize(QSize(80*dpiXFactor,80*dpiYFactor));
|
|
critical->setAutoFillBackground(false);
|
|
critical->setFocusPolicy(Qt::NoFocus);
|
|
|
|
QLabel *header = new QLabel(this);
|
|
header->setWordWrap(true);
|
|
header->setTextFormat(Qt::RichText);
|
|
header->setText(QString(tr("<center><h2>Upgrade of Athlete:<br>%1<br></h2></center>")).arg(athlete));
|
|
|
|
QLabel *text = new QLabel(this);
|
|
text->setWordWrap(true);
|
|
text->setTextFormat(Qt::RichText);
|
|
text->setText(tr("<center><b>Backup your 'Athlete' data first!<br>"
|
|
"<b>Please read carefully before proceeding!</b></center> <br> <br>"
|
|
"With Version 3.2 the 'Athlete' directory has been refactored "
|
|
"by adding a set of subdirectories which hold the different types "
|
|
"of GoldenCheetah files.<br><br>"
|
|
"The new structure is:<br>"
|
|
"-> Activity files: <samp>/activities</samp><br>"
|
|
"-> Configuration files: <samp>/config</samp><br>"
|
|
"-> Download files: <samp>/downloads</samp><br>"
|
|
"-> Import files: <samp>/imports</samp><br>"
|
|
"-> Backups of Activity files: <samp>/bak</samp><br>"
|
|
"-> Workout related files: <samp>/workouts</samp><br>"
|
|
"-> Cache files: <samp>/cache</samp><br>"
|
|
"-> Calendar files: <samp>/calendar</samp><br>"
|
|
"-> Log files: <samp>/logs</samp><br>"
|
|
"-> Temp files: <samp>/temp</samp><br>"
|
|
"-> Temp for Activities: <samp>/tempActivities</samp><br>"
|
|
"-> Train View recordings: <samp>/recordings</samp><br>"
|
|
"-> Quarantined files: <samp>/quarantine</samp><br><br>"
|
|
|
|
"The upgrade process will create the new directory structure and move "
|
|
"the existing files to the new directories as needed. During the upgrade "
|
|
"all activity files will be converted to GoldenCheetah's native "
|
|
"file format .JSON and moved to the <br><samp>/activities</samp> folder. The source files "
|
|
"are moved to the <samp>/imports</samp> folder.<br><br>"
|
|
"Starting with version 3.2 all downloads from devices or imported "
|
|
"activity files will be converted to GoldenCheetah's file "
|
|
"format during import/download. The original files will be stored - depending on the source - "
|
|
"in <samp>/downloads</samp> or <br><samp>/imports</samp> folder.<br><br>"
|
|
"<center><b>Please make sure that you have done a backup of your athlete data "
|
|
"before proceeding with the upgrade. We can't take responsibility for "
|
|
"any loss of data during the process. </b> </center> <br>"
|
|
));
|
|
scrollText = new QScrollArea();
|
|
scrollText->setWidget(text);
|
|
|
|
QLabel *footer1 = new QLabel(this);
|
|
footer1->setWordWrap(true);
|
|
footer1->setTextFormat(Qt::RichText);
|
|
footer1->setText(QString(tr("<center>Please backup the athlete directory:</center>")));
|
|
|
|
QLabel *footer2 = new QLabel(this);
|
|
footer2->setWordWrap(true);
|
|
footer2->setTextFormat(Qt::RichText);
|
|
footer2->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
footer2->setText(QString("<center><b>%1</b></center>").arg(athleteHomeDir.absolutePath()));
|
|
|
|
toprow->addWidget(critical);
|
|
toprow->addWidget(header);
|
|
layout->addLayout(toprow);
|
|
layout->addWidget(scrollText);
|
|
layout->addWidget(footer1);
|
|
layout->addWidget(footer2);
|
|
|
|
|
|
QHBoxLayout *lastRow = new QHBoxLayout;
|
|
|
|
proceedButton = new QPushButton(tr("Accept conditions and proceed with Upgrade"), this);
|
|
proceedButton->setEnabled(true);
|
|
connect(proceedButton, SIGNAL(clicked()), this, SLOT(accept()));
|
|
abortButton = new QPushButton(tr("Abort Upgrade"), this);
|
|
abortButton->setDefault(true);
|
|
connect(abortButton, SIGNAL(clicked()), this, SLOT(reject()));
|
|
|
|
|
|
lastRow->addWidget(abortButton);
|
|
lastRow->addWidget(proceedButton);
|
|
lastRow->addStretch();
|
|
layout->addLayout(lastRow);
|
|
|
|
}
|
|
|
|
|
|
|
|
GcUpgradeLogDialog::GcUpgradeLogDialog(QDir homeDir) : QDialog(NULL, Qt::Dialog), home(homeDir)
|
|
{
|
|
setAttribute(Qt::WA_DeleteOnClose, true);
|
|
setWindowTitle(QString(tr("Athlete %1").arg(home.root().dirName())));
|
|
|
|
QVBoxLayout *layout = new QVBoxLayout(this);
|
|
|
|
QHBoxLayout *toprow = new QHBoxLayout;
|
|
|
|
QPushButton *information = new QPushButton(style()->standardIcon(QStyle::SP_MessageBoxInformation), "", this);
|
|
information->setFixedSize(60*dpiXFactor,60*dpiYFactor);
|
|
information->setFlat(true);
|
|
information->setIconSize(QSize(60*dpiXFactor,60*dpiYFactor));
|
|
information->setAutoFillBackground(false);
|
|
information->setFocusPolicy(Qt::NoFocus);
|
|
|
|
QLabel *header = new QLabel(this);
|
|
header->setWordWrap(true);
|
|
header->setTextFormat(Qt::RichText);
|
|
header->setText(tr("<h1>Upgrade log: GoldenCheetah v3.2</h1>"));
|
|
|
|
toprow->addWidget(information);
|
|
toprow->addWidget(header);
|
|
layout->addLayout(toprow);
|
|
|
|
QFont defaultFont;
|
|
|
|
report = new QWebEngineView(this);
|
|
report->setContentsMargins(0,0,0,0);
|
|
report->page()->view()->setContentsMargins(0,0,0,0);
|
|
report->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
//XXX WEBENGINE report->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks);
|
|
report->setContextMenuPolicy(Qt::NoContextMenu);
|
|
report->setAcceptDrops(false);
|
|
report->settings()->setFontSize(QWebEngineSettings::DefaultFontSize, defaultFont.pointSize()+1);
|
|
report->settings()->setFontFamily(QWebEngineSettings::StandardFont, defaultFont.family());
|
|
|
|
connect(report, SIGNAL(linkClicked(QUrl)), this, SLOT(linkClickedSlot(QUrl)));
|
|
|
|
layout->addWidget(report);
|
|
|
|
QHBoxLayout *lastRow = new QHBoxLayout;
|
|
|
|
// create the buttons for the whole Dialog - but only make visible what is needed at the point in time
|
|
proceedButton = new QPushButton(tr("Proceed to Athlete"), this);
|
|
connect(proceedButton, SIGNAL(clicked()), this, SLOT(accept()));
|
|
saveAsButton = new QPushButton(tr("Save Upgrade Report..."), this);
|
|
saveAsButton->setDefault(true);
|
|
connect(saveAsButton, SIGNAL(clicked()), this, SLOT(saveAs()));
|
|
|
|
// during logging, disable the buttons, so user can't escape
|
|
proceedButton->setDisabled(true);
|
|
saveAsButton->setDisabled(true);
|
|
|
|
|
|
lastRow->addWidget(saveAsButton);
|
|
lastRow->addWidget(proceedButton);
|
|
lastRow->addStretch();
|
|
layout->addLayout(lastRow);
|
|
|
|
// the cyclist...
|
|
reportText += QString("<center><h1>Cyclist: \"%1\"</h1></center><br>").arg(home.root().dirName());
|
|
report->page()->setHtml(reportText);
|
|
}
|
|
|
|
GcUpgradeLogDialog::~GcUpgradeLogDialog()
|
|
{
|
|
if (report) delete report->page();
|
|
}
|
|
|
|
void
|
|
GcUpgradeLogDialog::saveAs()
|
|
{
|
|
QString fileName = QFileDialog::getSaveFileName( this, tr("Save Log"), QDir::homePath(), tr("Text File (*.txt)"));
|
|
|
|
// now write to it
|
|
QFile file(fileName);
|
|
file.resize(0);
|
|
QTextStream out(&file);
|
|
out.setCodec("UTF-8");
|
|
|
|
if (file.open(QIODevice::WriteOnly)) {
|
|
|
|
// write the texts
|
|
out << reportText;
|
|
|
|
}
|
|
file.close();
|
|
}
|
|
|
|
void
|
|
GcUpgradeLogDialog::linkClickedSlot( QUrl url )
|
|
{
|
|
QDesktopServices::openUrl( url );
|
|
|
|
}
|
|
|
|
|
|
void
|
|
GcUpgradeLogDialog::append(QString text, int level) {
|
|
|
|
switch (level) {
|
|
case 3:
|
|
reportText += QString("<h2>%1</h2>").arg(text);
|
|
break;
|
|
case 2:
|
|
reportText += QString("<h3>%1</h3>").arg(text);
|
|
break;
|
|
case 1:
|
|
reportText += QString("<h1>%1</h1>").arg(text);
|
|
break;
|
|
default: // any other
|
|
reportText += QString("%1 <br>").arg(text);
|
|
}
|
|
|
|
report->page()->setHtml(reportText);
|
|
//XXX WEBENGINE report->page()->setScrollBarValue(Qt::Vertical, report->page()->mainFrame()->scrollBarMaximum(Qt::Vertical));
|
|
this->repaint();
|
|
QApplication::processEvents();
|
|
|
|
}
|
|
|
|
void
|
|
GcUpgradeLogDialog::enableButtons() {
|
|
saveAsButton->setDisabled(false);
|
|
proceedButton->setDisabled(false);
|
|
|
|
}
|