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
+}