mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
... caused error in list-model since entries are added and "end-of" dbtable ... now only update of texts in upgrade to handle pre 3.3 translation problems
1024 lines
42 KiB
C++
1024 lines
42 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 <QDebug>
|
|
#include <QMessageBox>
|
|
#include <QFileDialog>
|
|
#include <QWebFrame>
|
|
#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();
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
// ... 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);
|
|
|
|
QVBoxLayout *layout = new QVBoxLayout(this);
|
|
|
|
QHBoxLayout *toprow = new QHBoxLayout;
|
|
|
|
QPushButton *critical = new QPushButton(style()->standardIcon(QStyle::SP_MessageBoxWarning), "", this);
|
|
critical->setFixedSize(80,80);
|
|
critical->setFlat(true);
|
|
critical->setIconSize(QSize(80,80));
|
|
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,60);
|
|
information->setFlat(true);
|
|
information->setIconSize(QSize(60,60));
|
|
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);
|
|
|
|
report = new QWebView(this);
|
|
report->setContentsMargins(0,0,0,0);
|
|
report->page()->view()->setContentsMargins(0,0,0,0);
|
|
report->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
report->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks);
|
|
report->setContextMenuPolicy(Qt::NoContextMenu);
|
|
report->setAcceptDrops(false);
|
|
connect( report, SIGNAL( linkClicked( QUrl ) ), this, SLOT( linkClickedSlot( QUrl ) ) );
|
|
|
|
QFont defaultFont; // mainwindow sets up the defaults.. we need to apply
|
|
report->settings()->setFontSize(QWebSettings::DefaultFontSize, defaultFont.pointSize()+1);
|
|
report->settings()->setFontFamily(QWebSettings::StandardFont, defaultFont.family());
|
|
|
|
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()->mainFrame()->setHtml(reportText);
|
|
|
|
}
|
|
|
|
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 << report->page()->mainFrame()->toPlainText();
|
|
|
|
}
|
|
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()->mainFrame()->setHtml(reportText);
|
|
report->page()->mainFrame()->setScrollBarValue(Qt::Vertical, report->page()->mainFrame()->scrollBarMaximum(Qt::Vertical));
|
|
this->repaint();
|
|
QApplication::processEvents();
|
|
|
|
}
|
|
|
|
void
|
|
GcUpgradeLogDialog::enableButtons() {
|
|
saveAsButton->setDisabled(false);
|
|
proceedButton->setDisabled(false);
|
|
|
|
}
|