From 7468ed7dc04ab1d027caa56dbb1a6efd549b2297 Mon Sep 17 00:00:00 2001 From: Michal Horejsek Date: Wed, 27 Jan 2021 09:16:04 +0100 Subject: [PATCH] GODT-976 Exclude updates from clearing cache and clear cache, including updates, while switching early access off --- internal/app/bridge/bridge.go | 2 +- internal/bridge/bridge.go | 42 +++++++++++++++++++ internal/bridge/types.go | 17 ++++++++ .../frontend/qml/BridgeUI/DialogYesNo.qml | 16 ++++++- .../frontend/qml/BridgeUI/SettingsView.qml | 5 ++- internal/frontend/qt/accounts.go | 10 +++++ internal/frontend/qt/frontend.go | 18 +++++--- internal/frontend/types/types.go | 2 + internal/locations/locations.go | 10 ++++- internal/locations/locations_test.go | 14 ++++++- internal/updater/updater.go | 4 ++ internal/versioner/remove_darwin.go | 8 ++++ internal/versioner/remove_default.go | 20 +++++++++ internal/versioner/version.go | 4 ++ test/context/bridge.go | 4 +- test/context/locations.go | 4 ++ test/context/updater.go | 41 ++++++++++++++++++ test/context/versioner.go | 33 +++++++++++++++ 18 files changed, 241 insertions(+), 13 deletions(-) create mode 100644 test/context/updater.go create mode 100644 test/context/versioner.go diff --git a/internal/app/bridge/bridge.go b/internal/app/bridge/bridge.go index 0d0ed855..caa74d9d 100644 --- a/internal/app/bridge/bridge.go +++ b/internal/app/bridge/bridge.go @@ -65,7 +65,7 @@ func run(b *base.Base, c *cli.Context) error { // nolint[funlen] logrus.WithError(err).Fatal("Failed to load TLS config") } - bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.CrashHandler, b.Listener, b.CM, b.Creds) + bridge := bridge.New(b.Locations, b.Cache, b.Settings, b.CrashHandler, b.Listener, b.CM, b.Creds, b.Updater, b.Versioner) imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, bridge) smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge) diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index 86a06560..6f46bf4c 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -26,6 +26,7 @@ import ( "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/constants" "github.com/ProtonMail/proton-bridge/internal/metrics" + "github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/pkg/pmapi" @@ -40,8 +41,11 @@ var ( type Bridge struct { *users.Users + locations Locator settings SettingsProvider clientManager users.ClientManager + updater Updater + versioner Versioner userAgentClientName string userAgentClientVersion string @@ -56,6 +60,8 @@ func New( eventListener listener.Listener, clientManager users.ClientManager, credStorer users.CredentialsStorer, + updater Updater, + versioner Versioner, ) *Bridge { // Allow DoH before starting the app if the user has previously set this setting. // This allows us to start even if protonmail is blocked. @@ -68,8 +74,11 @@ func New( b := &Bridge{ Users: u, + locations: locations, settings: s, clientManager: clientManager, + updater: updater, + versioner: versioner, } if s.GetBool(settings.FirstStartKey) { @@ -168,3 +177,36 @@ func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, return nil } + +// GetUpdateChannel returns currently set update channel. +func (b *Bridge) GetUpdateChannel() updater.UpdateChannel { + return updater.UpdateChannel(b.settings.Get(settings.UpdateChannelKey)) +} + +// SetUpdateChannel switches update channel. +// Downgrading to previous version (by switching from early to stable, for example) +// requires clearing all data including update files due to possibility of +// inconsistency between versions and absence of backwards migration scripts. +func (b *Bridge) SetUpdateChannel(channel updater.UpdateChannel) error { + b.settings.Set(settings.UpdateChannelKey, string(channel)) + + version, err := b.updater.Check() + if err != nil { + return err + } + + if b.updater.IsDowngrade(version) { + if err := b.Users.ClearData(); err != nil { + log.WithError(err).Error("Failed to clear data while downgrading channel") + } + if err := b.locations.ClearUpdates(); err != nil { + log.WithError(err).Error("Failed to clear updates while downgrading channel") + } + } + + if err := b.updater.InstallUpdate(version); err != nil { + return err + } + + return b.versioner.RemoveOtherVersions(version.Version) +} diff --git a/internal/bridge/types.go b/internal/bridge/types.go index 51e7d231..4b3cb701 100644 --- a/internal/bridge/types.go +++ b/internal/bridge/types.go @@ -17,8 +17,15 @@ package bridge +import ( + "github.com/Masterminds/semver/v3" + + "github.com/ProtonMail/proton-bridge/internal/updater" +) + type Locator interface { Clear() error + ClearUpdates() error } type Cacher interface { @@ -32,3 +39,13 @@ type SettingsProvider interface { GetBool(key string) bool SetBool(key string, val bool) } + +type Updater interface { + Check() (updater.VersionInfo, error) + IsDowngrade(updater.VersionInfo) bool + InstallUpdate(updater.VersionInfo) error +} + +type Versioner interface { + RemoveOtherVersions(*semver.Version) error +} diff --git a/internal/frontend/qml/BridgeUI/DialogYesNo.qml b/internal/frontend/qml/BridgeUI/DialogYesNo.qml index 6086ed90..d6f4ac30 100644 --- a/internal/frontend/qml/BridgeUI/DialogYesNo.qml +++ b/internal/frontend/qml/BridgeUI/DialogYesNo.qml @@ -294,7 +294,7 @@ Dialog { } }, State { - name: "toggleEarlyAccess" + name: "toggleEarlyAccessOn" PropertyChanges { target: root currentIndex : 0 @@ -304,6 +304,17 @@ Dialog { answer : qsTr("Enabling early access...") } }, + State { + name: "toggleEarlyAccessOff" + PropertyChanges { + target: root + currentIndex : 0 + question : qsTr("Are you sure you want to leave early access? Please keep in mind this operation clears the cache and restarts Bridge.") + note : qsTr("This will delete all of your stored preferences as well as cached email data for all accounts, temporarily slowing down the email download process significantly.") + title : qsTr("Disable early access") + answer : qsTr("Disabling early access...") + } + }, State { name: "noKeychain" PropertyChanges { @@ -375,7 +386,8 @@ Dialog { if ( state == "logout" ) { go.logoutAccount (input) } if ( state == "toggleAutoStart" ) { go.toggleAutoStart () } if ( state == "toggleAllowProxy" ) { go.toggleAllowProxy () } - if ( state == "toggleEarlyAccess" ) { go.toggleEarlyAccess () } + if ( state == "toggleEarlyAccessOn" ) { go.toggleEarlyAccess () } + if ( state == "toggleEarlyAccessOff" ) { go.toggleEarlyAccess () } if ( state == "quit" ) { Qt.quit () } if ( state == "instance exists" ) { Qt.quit () } if ( state == "noKeychain" ) { Qt.quit () } diff --git a/internal/frontend/qml/BridgeUI/SettingsView.qml b/internal/frontend/qml/BridgeUI/SettingsView.qml index 41f2ef80..d16f0298 100644 --- a/internal/frontend/qml/BridgeUI/SettingsView.qml +++ b/internal/frontend/qml/BridgeUI/SettingsView.qml @@ -150,9 +150,10 @@ Item { ) + " " + text onClicked: { if (go.isEarlyAccess == true) { - go.toggleEarlyAccess() + dialogGlobal.state="toggleEarlyAccessOff" + dialogGlobal.show() } else { - dialogGlobal.state="toggleEarlyAccess" + dialogGlobal.state="toggleEarlyAccessOn" dialogGlobal.show() } } diff --git a/internal/frontend/qt/accounts.go b/internal/frontend/qt/accounts.go index a86aacd5..296f6af6 100644 --- a/internal/frontend/qt/accounts.go +++ b/internal/frontend/qt/accounts.go @@ -26,6 +26,7 @@ import ( "github.com/ProtonMail/proton-bridge/internal/bridge" "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/events" + "github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/pkg/keychain" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" ) @@ -80,6 +81,15 @@ func (s *FrontendQt) loadAccounts() { func (s *FrontendQt) clearCache() { defer s.Qml.ProcessFinished() + + channel := s.bridge.GetUpdateChannel() + if channel == updater.EarlyChannel { + if err := s.bridge.SetUpdateChannel(updater.StableChannel); err != nil { + s.Qml.NotifyManualUpdateError() + return + } + } + if err := s.bridge.ClearData(); err != nil { log.Error("While clearing cache: ", err) } diff --git a/internal/frontend/qt/frontend.go b/internal/frontend/qt/frontend.go index 4eff24be..a126875f 100644 --- a/internal/frontend/qt/frontend.go +++ b/internal/frontend/qt/frontend.go @@ -578,13 +578,21 @@ func (s *FrontendQt) toggleAutoUpdate() { func (s *FrontendQt) toggleEarlyAccess() { defer s.Qml.ProcessFinished() - if updater.UpdateChannel(s.settings.Get(settings.UpdateChannelKey)) == updater.EarlyChannel { - s.settings.Set(settings.UpdateChannelKey, string(updater.StableChannel)) - s.Qml.SetIsEarlyAccess(false) + channel := s.bridge.GetUpdateChannel() + if channel == updater.EarlyChannel { + channel = updater.StableChannel } else { - s.settings.Set(settings.UpdateChannelKey, string(updater.EarlyChannel)) - s.Qml.SetIsEarlyAccess(true) + channel = updater.EarlyChannel } + + err := s.bridge.SetUpdateChannel(channel) + s.Qml.SetIsEarlyAccess(channel == updater.EarlyChannel) + if err != nil { + s.Qml.NotifyManualUpdateError() + return + } + s.restarter.SetToRestart() + s.App.Quit() } func (s *FrontendQt) toggleAllowProxy() { diff --git a/internal/frontend/types/types.go b/internal/frontend/types/types.go index 24158812..54f6f43e 100644 --- a/internal/frontend/types/types.go +++ b/internal/frontend/types/types.go @@ -80,6 +80,8 @@ type Bridger interface { ReportBug(osType, osVersion, description, accountName, address, emailClient string) error AllowProxy() DisallowProxy() + GetUpdateChannel() updater.UpdateChannel + SetUpdateChannel(updater.UpdateChannel) error } type bridgeWrap struct { diff --git a/internal/locations/locations.go b/internal/locations/locations.go index 74a026f6..bc851a2d 100644 --- a/internal/locations/locations.go +++ b/internal/locations/locations.go @@ -165,12 +165,20 @@ func (l *Locations) getUpdatesPath() string { return filepath.Join(l.userCache, "updates") } -// Clear removes everything except the lock file. +// Clear removes everything except the lock and update files. func (l *Locations) Clear() error { return files.Remove( l.getSettingsPath(), l.getLogsPath(), l.getCachePath(), + ).Except( + l.getUpdatesPath(), + ).Do() +} + +// ClearUpdates removes update files. +func (l *Locations) ClearUpdates() error { + return files.Remove( l.getUpdatesPath(), ).Do() } diff --git a/internal/locations/locations_test.go b/internal/locations/locations_test.go index 888d8515..1340794a 100644 --- a/internal/locations/locations_test.go +++ b/internal/locations/locations_test.go @@ -39,7 +39,7 @@ func (dirs *fakeAppDirs) UserCache() string { return dirs.cacheDir } -func TestClearRemovesEverythingExceptLockFile(t *testing.T) { +func TestClearRemovesEverythingExceptLockAndUpdateFiles(t *testing.T) { l := newTestLocations(t) assert.NoError(t, l.Clear()) @@ -48,6 +48,18 @@ func TestClearRemovesEverythingExceptLockFile(t *testing.T) { assert.NoDirExists(t, l.getSettingsPath()) assert.NoDirExists(t, l.getLogsPath()) assert.NoDirExists(t, l.getCachePath()) + assert.DirExists(t, l.getUpdatesPath()) +} + +func TestClearUpdateFiles(t *testing.T) { + l := newTestLocations(t) + + assert.NoError(t, l.ClearUpdates()) + + assert.FileExists(t, l.GetLockFile()) + assert.DirExists(t, l.getSettingsPath()) + assert.DirExists(t, l.getLogsPath()) + assert.DirExists(t, l.getCachePath()) assert.NoDirExists(t, l.getUpdatesPath()) } diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 65297c22..e588e99b 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -125,6 +125,10 @@ func (u *Updater) IsUpdateApplicable(version VersionInfo) bool { return true } +func (u *Updater) IsDowngrade(version VersionInfo) bool { + return version.Version.LessThan(u.curVer) +} + func (u *Updater) CanInstall(version VersionInfo) bool { if version.MinAuto == nil { return true diff --git a/internal/versioner/remove_darwin.go b/internal/versioner/remove_darwin.go index c6d5324e..f9a1b05b 100644 --- a/internal/versioner/remove_darwin.go +++ b/internal/versioner/remove_darwin.go @@ -17,8 +17,16 @@ package versioner +import "github.com/Masterminds/semver/v3" + // RemoveOldVersions removes all but the latest app version. func (v *Versioner) RemoveOldVersions() error { // darwin does not use the versioner; removal is a noop. return nil } + +// RemoveOtherVersions removes all but the specific provided app version. +func (v *Versioner) RemoveOtherVersions(versionToKeep *semver.Version) error { + // darwin does not use the versioner; removal is a noop. + return nil +} diff --git a/internal/versioner/remove_default.go b/internal/versioner/remove_default.go index a9ad802c..3e7ae2a4 100644 --- a/internal/versioner/remove_default.go +++ b/internal/versioner/remove_default.go @@ -22,6 +22,7 @@ package versioner import ( "os" + "github.com/Masterminds/semver/v3" "github.com/sirupsen/logrus" ) @@ -45,3 +46,22 @@ func (v *Versioner) RemoveOldVersions() error { return nil } + +// RemoveOtherVersions removes all but the specific provided app version. +func (v *Versioner) RemoveOtherVersions(versionToKeep *semver.Version) error { + versions, err := v.ListVersions() + if err != nil { + return err + } + + for _, version := range versions { + if version.Equal(versionToKeep) { + continue + } + if err := os.RemoveAll(version.path); err != nil { + logrus.WithError(err).Error("Failed to remove old app version") + } + } + + return nil +} diff --git a/internal/versioner/version.go b/internal/versioner/version.go index 376f0eb6..5a3c9613 100644 --- a/internal/versioner/version.go +++ b/internal/versioner/version.go @@ -55,6 +55,10 @@ func (v *Version) String() string { return fmt.Sprintf("%v", v.version) } +func (v *Version) Equal(version *semver.Version) bool { + return v.version.Equal(version) +} + // VerifyFiles verifies all files in the version directory. func (v *Version) VerifyFiles(kr *crypto.KeyRing) error { fileBytes, err := ioutil.ReadFile(filepath.Join(v.path, sumFile)) // nolint[gosec] diff --git a/test/context/bridge.go b/test/context/bridge.go index 40740aad..6664b12e 100644 --- a/test/context/bridge.go +++ b/test/context/bridge.go @@ -68,5 +68,7 @@ func newBridgeInstance( clientManager users.ClientManager, ) *bridge.Bridge { panicHandler := &panicHandler{t: t} - return bridge.New(locations, cache, settings, panicHandler, eventListener, clientManager, credStore) + updater := newFakeUpdater() + versioner := newFakeVersioner() + return bridge.New(locations, cache, settings, panicHandler, eventListener, clientManager, credStore, updater, versioner) } diff --git a/test/context/locations.go b/test/context/locations.go index afeeefb2..6db69042 100644 --- a/test/context/locations.go +++ b/test/context/locations.go @@ -48,3 +48,7 @@ func (l *fakeLocations) ProvideSettingsPath() (string, error) { func (l *fakeLocations) Clear() error { return os.RemoveAll(l.dir) } + +func (l *fakeLocations) ClearUpdates() error { + return nil +} diff --git a/test/context/updater.go b/test/context/updater.go new file mode 100644 index 00000000..0c1094e6 --- /dev/null +++ b/test/context/updater.go @@ -0,0 +1,41 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge.Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +package context + +import ( + "github.com/ProtonMail/proton-bridge/internal/updater" +) + +type fakeUpdater struct{} + +// newFakeUpdater creates an empty updater just to fulfill Bridge dependencies. +func newFakeUpdater() *fakeUpdater { + return &fakeUpdater{} +} + +func (c *fakeUpdater) Check() (updater.VersionInfo, error) { + return updater.VersionInfo{}, nil +} + +func (c *fakeUpdater) IsDowngrade(_ updater.VersionInfo) bool { + return false +} + +func (c *fakeUpdater) InstallUpdate(_ updater.VersionInfo) error { + return nil +} diff --git a/test/context/versioner.go b/test/context/versioner.go new file mode 100644 index 00000000..8ee14382 --- /dev/null +++ b/test/context/versioner.go @@ -0,0 +1,33 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge.Bridge. +// +// ProtonMail Bridge 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 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge 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 ProtonMail Bridge. If not, see . + +package context + +import ( + "github.com/Masterminds/semver/v3" +) + +type fakeVersioner struct{} + +// newFakeVersioner creates an empty versioner just to fulfill Bridge dependencies. +func newFakeVersioner() *fakeVersioner { + return &fakeVersioner{} +} + +func (c *fakeVersioner) RemoveOtherVersions(_ *semver.Version) error { + return nil +}