forked from Silverfish/proton-bridge
Launcher, app/base, sentry, update service
This commit is contained in:
22
internal/updater/channel_beta.go
Normal file
22
internal/updater/channel_beta.go
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build beta
|
||||
|
||||
package updater
|
||||
|
||||
const Channel = "beta"
|
||||
24
internal/updater/channel_default.go
Normal file
24
internal/updater/channel_default.go
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !beta
|
||||
|
||||
package updater
|
||||
|
||||
// Channel is the channel of updates users are subscribed to.
|
||||
// For now it is hardcoded in the build. In future, it might be selectable in settings.
|
||||
const Channel = "live"
|
||||
22
internal/updater/host_default.go
Normal file
22
internal/updater/host_default.go
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !pmapi_qa
|
||||
|
||||
package updater
|
||||
|
||||
const Host = "https://protonmail.com/download"
|
||||
22
internal/updater/host_qa.go
Normal file
22
internal/updater/host_qa.go
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build pmapi_qa
|
||||
|
||||
package updater
|
||||
|
||||
const Host = "https://bridgeteam.protontech.ch/bridgeteam/autoupdates/download"
|
||||
64
internal/updater/install_darwin.go
Normal file
64
internal/updater/install_darwin.go
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/internal/versioner"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/tar"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Installer struct{}
|
||||
|
||||
func NewInstaller(*versioner.Versioner) *Installer {
|
||||
return &Installer{}
|
||||
}
|
||||
|
||||
func (i *Installer) InstallUpdate(_ *semver.Version, r io.Reader) error {
|
||||
gr, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = gr.Close() }()
|
||||
|
||||
tempDir, err := ioutil.TempDir("", "proton-update-source")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get temporary update directory")
|
||||
}
|
||||
|
||||
if err := tar.UntarToDir(gr, tempDir); err != nil {
|
||||
return errors.Wrap(err, "failed to unpack update package")
|
||||
}
|
||||
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to determine current executable path")
|
||||
}
|
||||
|
||||
oldBundle := filepath.Dir(filepath.Dir(filepath.Dir(exePath)))
|
||||
newBundle := filepath.Join(tempDir, filepath.Base(oldBundle))
|
||||
|
||||
return syncFolders(oldBundle, newBundle)
|
||||
}
|
||||
41
internal/updater/install_default.go
Normal file
41
internal/updater/install_default.go
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !darwin
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/internal/versioner"
|
||||
)
|
||||
|
||||
type Installer struct {
|
||||
versioner *versioner.Versioner
|
||||
}
|
||||
|
||||
func NewInstaller(versioner *versioner.Versioner) *Installer {
|
||||
return &Installer{
|
||||
versioner: versioner,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Installer) InstallUpdate(version *semver.Version, r io.Reader) error {
|
||||
return i.versioner.InstallNewVersion(version, r)
|
||||
}
|
||||
75
internal/updater/key_default.go
Normal file
75
internal/updater/key_default.go
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !pmapi_qa
|
||||
|
||||
package updater
|
||||
|
||||
// DefaultPublicKey is the public key used to sign builds.
|
||||
const DefaultPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
qdwhrw==
|
||||
=mu62
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
52
internal/updater/key_qa.go
Normal file
52
internal/updater/key_qa.go
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build pmapi_qa
|
||||
|
||||
package updater
|
||||
|
||||
// DefaultPublicKey is the public key used to sign builds.
|
||||
const DefaultPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBF9Q55wBCADiwBHGCyJiO2ZSDh9ZPecFKnf+JEryzqGYu3jImEoV2X5Bx/Kl
|
||||
5n3hHvao9jekEDFr1AjvSKfG9Zz/1GdionUUEdw76mkc7y09GKdXENOyCQYs7CV7
|
||||
WbWDSGSmp6DVBcRzRzMKm4zuB208a6Wwd2aYqIJ9Oo0l3ypQnox0BQCbbqewYSYN
|
||||
Dmj+WJkO+e2ovJQWrQgtpnj/QBX18KBjP4FiLSPHAyy7aC2t6JlTIz8UVAw2VZFn
|
||||
GBUUqnn0iy3W0nJNgv1ouo0rCa+eYBpz3n+GKTFWFDTIPQfZbh15nFJJgBSuiwyM
|
||||
sHjWCNJYu5PQmwNlGJJjtKw/9xgTFLC9yaNPABEBAAG0BkJyaWRnZYkBTgQTAQgA
|
||||
OBYhBH3hU445a9yHH+QknbtAQ7nyijPUBQJfUOecAhsDBQsJCAcCBhUKCQgLAgQW
|
||||
AgMBAh4BAheAAAoJELtAQ7nyijPUpisH/iznWGoma1PXpaQlD2241k9zSzg3Nczn
|
||||
yfm2mYtXlGVvjGLr29neErWpLy0Kb2ihKTTsgMkwSwcasBap8HYTtENNl1nUzQL7
|
||||
UhaASTzZ2jYw4Dypps+DYpoLm9RUWKHuUOE5Ov8QPjTBC/BswA0Lv1Z9u9t5qsdp
|
||||
UgB+YVYgRC+zSHMIzWSMx0dCSPgRilkPvIa5wB77J1+ZE7y1n/uQXOYrKitWrf+w
|
||||
tXcRYoPqYQ4KXIQ/PMCTwSEDDbsPD7F09AzYQPv6D20d7dyEf0/hlfpj+cvGyBG0
|
||||
GdGLjwjjKNA99ra1IXjgBUIEv/XpijfKK2D0FDiOdZi3JnVr8OYBCeW5AQ0EX1Dn
|
||||
nAEIAMtD5sLJ3hXE/bKRQaINx+7hzYhFOxzdGdOTlzlzEjsWYLmy2cWb2fjazIhf
|
||||
37g8HlSlMaHtHkdJIn1hS9+N76GxEChH31tF6Cuyz+k6TRqroNHsIxzOIjv3+qkM
|
||||
7xWPRhq8msB8ulWKBQtWpwVVC3sa/qTh9k29wuEiwQY0IxLV0a6BkE1TqK5/7A6Q
|
||||
o8SMCvQW6wAxPZMhPM/FwxMYxrKUT3UUDmRYS5RvSlMGUwK2HucQVU/qwsOPkJs4
|
||||
wq6RI+5NDtyGxMxUKod/GYpPaICUI/VNgIZXX6NNzS7JYEYBjtI/JOEOc0yQSh1u
|
||||
jEGl1k+4OLogUiV02mpGCrHutm0AEQEAAYkBNgQYAQgAIBYhBH3hU445a9yHH+Qk
|
||||
nbtAQ7nyijPUBQJfUOecAhsMAAoJELtAQ7nyijPU/wUIAKibg4GFxHFSiEjtzdlO
|
||||
2cIIr3yCsFmGFYVLF3JkOtVvQk7QDZTNsx5ZqC+Mtlf3Z04btG5M/FpHQ097orfl
|
||||
IH+bZVXMrYtzd4J7ujKGEJU2hY6a9j50odsiwl6CSrXdppS7RGdkhui0RCke/y9Z
|
||||
wJU5oyiWmcsQfhnET7DEpI7twqEwg43VBGOnaRxKFecyYsQVASlrWMENEpoaup8B
|
||||
oIS2nDvMVSSK77tmkNcLt8911VqZPtOYmxzM5rc+gm7Pn9kSZUXoGy4p5sFDu/mj
|
||||
zT1w+Qev2GlSVwFdKPasefLmb3lBEbNeZAkfFl48WEzwtK3VJM60Xl8RPFk0IKLe
|
||||
tXw=
|
||||
=aaxG
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
50
internal/updater/locker.go
Normal file
50
internal/updater/locker.go
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrOperationOngoing = errors.New("the operation is already ongoing")
|
||||
|
||||
// locker is an easy way to ensure we only perform one update at a time.
|
||||
type locker struct {
|
||||
ongoing atomic.Value
|
||||
}
|
||||
|
||||
func newLocker() *locker {
|
||||
l := &locker{}
|
||||
|
||||
l.ongoing.Store(false)
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *locker) doOnce(fn func() error) error {
|
||||
if l.ongoing.Load().(bool) {
|
||||
return ErrOperationOngoing
|
||||
}
|
||||
|
||||
l.ongoing.Store(true)
|
||||
defer func() { l.ongoing.Store(false) }()
|
||||
|
||||
return fn()
|
||||
}
|
||||
67
internal/updater/locker_test.go
Normal file
67
internal/updater/locker_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLocker(t *testing.T) {
|
||||
l := newLocker()
|
||||
|
||||
assert.NoError(t, l.doOnce(func() error {
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
func TestLockerForwardsErrors(t *testing.T) {
|
||||
l := newLocker()
|
||||
|
||||
assert.Error(t, l.doOnce(func() error {
|
||||
return errors.New("something went wrong")
|
||||
}))
|
||||
}
|
||||
|
||||
func TestLockerAllowsOnlyOneOperation(t *testing.T) {
|
||||
l := newLocker()
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
assert.NoError(t, l.doOnce(func() error {
|
||||
time.Sleep(2 * time.Second)
|
||||
wg.Done()
|
||||
return nil
|
||||
}))
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
err := l.doOnce(func() error { return nil })
|
||||
if assert.Error(t, err) {
|
||||
assert.Equal(t, ErrOperationOngoing, err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
254
internal/updater/sync.go
Normal file
254
internal/updater/sync.go
Normal file
@ -0,0 +1,254 @@
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func syncFolders(localPath, updatePath string) (err error) {
|
||||
backupDir := filepath.Join(filepath.Dir(updatePath), "backup")
|
||||
if err = createBackup(localPath, backupDir); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = removeMissing(localPath, updatePath); err != nil {
|
||||
restoreFromBackup(backupDir, localPath)
|
||||
return
|
||||
}
|
||||
|
||||
if err = copyRecursively(updatePath, localPath); err != nil {
|
||||
restoreFromBackup(backupDir, localPath)
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeMissing(folderToCleanPath, itemsToKeepPath string) (err error) {
|
||||
logrus.WithField("from", folderToCleanPath).Debug("Remove missing")
|
||||
// Create list of files.
|
||||
existingRelPaths := map[string]bool{}
|
||||
err = filepath.Walk(itemsToKeepPath, func(keepThis string, _ os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
relPath, walkErr := filepath.Rel(itemsToKeepPath, keepThis)
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
logrus.WithField("path", relPath).Trace("Keep the path")
|
||||
existingRelPaths[relPath] = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
delList := []string{}
|
||||
err = filepath.Walk(folderToCleanPath, func(removeThis string, _ os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
relPath, walkErr := filepath.Rel(folderToCleanPath, removeThis)
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
logrus.Debug("check path ", relPath)
|
||||
if !existingRelPaths[relPath] {
|
||||
logrus.Debug("path not in list, removing ", removeThis)
|
||||
delList = append(delList, removeThis)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, removeThis := range delList {
|
||||
if err = os.RemoveAll(removeThis); err != nil && !os.IsNotExist(err) {
|
||||
logrus.Error("remove error ", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreFromBackup(backupDir, localPath string) {
|
||||
logrus.WithField("from", backupDir).
|
||||
WithField("to", localPath).
|
||||
Error("recovering")
|
||||
if err := copyRecursively(backupDir, localPath); err != nil {
|
||||
logrus.WithField("from", backupDir).
|
||||
WithField("to", localPath).
|
||||
Error("Not able to recover.")
|
||||
}
|
||||
}
|
||||
|
||||
func createBackup(srcFile, dstDir string) (err error) {
|
||||
logrus.WithField("from", srcFile).WithField("to", dstDir).Debug("Create backup")
|
||||
if err = mkdirAllClear(dstDir); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return copyRecursively(srcFile, dstDir)
|
||||
}
|
||||
|
||||
func mkdirAllClear(path string) error {
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.MkdirAll(path, 0750)
|
||||
}
|
||||
|
||||
// checksum assumes the file is a regular file and that it exists.
|
||||
func checksum(path string) (hash string) {
|
||||
file, err := os.Open(path) //nolint[gosec]
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close() //nolint[errcheck]
|
||||
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return string(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
// srcDir including app folder.
|
||||
// dstDir including app folder.
|
||||
func copyRecursively(srcDir, dstDir string) error { // nolint[funlen]
|
||||
return filepath.Walk(srcDir, func(srcPath string, srcInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcIsLink := srcInfo.Mode()&os.ModeSymlink == os.ModeSymlink
|
||||
srcIsDir := srcInfo.IsDir()
|
||||
|
||||
// Non regular source (e.g. named pipes, sockets, devices...).
|
||||
if !srcIsLink && !srcIsDir && !srcInfo.Mode().IsRegular() {
|
||||
logrus.Error("File ", srcPath, " with mode ", srcInfo.Mode())
|
||||
return errors.New("irregular source file. Copy not implemented")
|
||||
}
|
||||
|
||||
// Destination path.
|
||||
srcRelPath, err := filepath.Rel(srcDir, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstPath := filepath.Join(dstDir, srcRelPath)
|
||||
logrus.Debug("src: ", srcPath, " dst: ", dstPath)
|
||||
|
||||
// Destination exists.
|
||||
dstInfo, err := os.Lstat(dstPath)
|
||||
if err == nil {
|
||||
dstIsLink := dstInfo.Mode()&os.ModeSymlink == os.ModeSymlink
|
||||
dstIsDir := dstInfo.IsDir()
|
||||
|
||||
// Non regular destination (e.g. named pipes, sockets, devices...).
|
||||
if !dstIsLink && !dstIsDir && !dstInfo.Mode().IsRegular() {
|
||||
logrus.Error("File ", dstPath, " with mode ", dstInfo.Mode())
|
||||
return errors.New("irregular target file. Copy not implemented")
|
||||
}
|
||||
|
||||
if dstIsLink {
|
||||
if err = os.Remove(dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !dstIsLink && dstIsDir && !srcIsDir {
|
||||
if err = os.RemoveAll(dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Do not return if !dstIsLink && dstIsDir && srcIsDir: the permissions might change.
|
||||
|
||||
if dstInfo.Mode().IsRegular() && !srcInfo.Mode().IsRegular() {
|
||||
if err = os.Remove(dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create symbolic link and return.
|
||||
if srcIsLink {
|
||||
logrus.Debug("It is a symlink")
|
||||
linkPath, err := os.Readlink(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Debug("link to ", linkPath)
|
||||
return os.Symlink(linkPath, dstPath)
|
||||
}
|
||||
|
||||
// Create dir and return.
|
||||
if srcIsDir {
|
||||
logrus.Debug("It is a dir")
|
||||
return os.MkdirAll(dstPath, srcInfo.Mode())
|
||||
}
|
||||
|
||||
// Regular files only.
|
||||
// If files are same return.
|
||||
if os.SameFile(srcInfo, dstInfo) || checksum(srcPath) == checksum(dstPath) {
|
||||
logrus.Debug("Same files, skip copy")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create/overwrite regular file.
|
||||
srcReader, err := os.Open(srcPath) //nolint[gosec]
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcReader.Close() //nolint[errcheck]
|
||||
return copyToTmpFileRename(srcReader, dstPath, srcInfo.Mode())
|
||||
})
|
||||
}
|
||||
|
||||
func copyToTmpFileRename(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
|
||||
logrus.Debug("Tmp and rename ", dstPath)
|
||||
tmpPath := dstPath + ".tmp"
|
||||
if err := copyToFileTruncate(srcReader, tmpPath, dstMode); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmpPath, dstPath)
|
||||
}
|
||||
|
||||
func copyToFileTruncate(srcReader io.Reader, dstPath string, dstMode os.FileMode) error {
|
||||
logrus.Debug("Copy and truncate ", dstPath)
|
||||
dstWriter, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, dstMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstWriter.Close() //nolint[errcheck]
|
||||
_, err = io.Copy(dstWriter, srcReader)
|
||||
return err
|
||||
}
|
||||
157
internal/updater/sync_test.go
Normal file
157
internal/updater/sync_test.go
Normal file
@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
FileType = "File"
|
||||
SymlinkType = "Symlink"
|
||||
DirType = "Dir"
|
||||
EmptyType = "Empty"
|
||||
NewType = "New"
|
||||
)
|
||||
|
||||
func TestSyncFolder(t *testing.T) {
|
||||
for _, srcType := range []string{EmptyType, FileType, SymlinkType, DirType} {
|
||||
for _, dstType := range []string{EmptyType, FileType, SymlinkType, DirType} {
|
||||
require.NoError(t, checkCopyWorks(srcType, dstType))
|
||||
logrus.Warn("OK: from ", srcType, " to ", dstType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkCopyWorks(srcType, dstType string) error {
|
||||
dirName := "from_" + srcType + "_to_" + dstType
|
||||
AppCacheDir := "/tmp"
|
||||
srcDir := filepath.Join(AppCacheDir, "sync_src", dirName)
|
||||
destDir := filepath.Join(AppCacheDir, "sync_dst", dirName)
|
||||
|
||||
// clear before
|
||||
logrus.Info("remove all ", srcDir)
|
||||
err := os.RemoveAll(srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Info("remove all ", destDir)
|
||||
err = os.RemoveAll(destDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create
|
||||
err = createTestFolder(srcDir, srcType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = createTestFolder(destDir, dstType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// copy
|
||||
logrus.Info("Sync from ", srcDir, " to ", destDir)
|
||||
err = syncFolders(destDir, srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check
|
||||
logrus.Info("check ", srcDir, " and ", destDir)
|
||||
err = checkThatFilesAreSame(srcDir, destDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// clear after
|
||||
logrus.Info("remove all ", srcDir)
|
||||
err = os.RemoveAll(srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Info("remove all ", destDir)
|
||||
err = os.RemoveAll(destDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func checkThatFilesAreSame(src, dst string) error {
|
||||
cmd := exec.Command("diff", "-qr", src, dst) //nolint[gosec]
|
||||
cmd.Stderr = logrus.StandardLogger().WriterLevel(logrus.ErrorLevel)
|
||||
cmd.Stdout = logrus.StandardLogger().WriterLevel(logrus.InfoLevel)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func createTestFolder(dirPath, dirType string) error {
|
||||
logrus.Info("creating folder ", dirPath, " type ", dirType)
|
||||
if dirType == NewType {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := mkdirAllClear(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dirType == EmptyType {
|
||||
return nil
|
||||
}
|
||||
|
||||
path := filepath.Join(dirPath, "testpath")
|
||||
switch dirType {
|
||||
case FileType:
|
||||
err = ioutil.WriteFile(path, []byte("This is a test"), 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case SymlinkType:
|
||||
err = os.Symlink("../../", path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case DirType:
|
||||
err = os.MkdirAll(path, 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(path, "another_file"), []byte("This is a test"), 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
167
internal/updater/updater.go
Normal file
167
internal/updater/updater.go
Normal file
@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type clientProvider interface {
|
||||
GetAnonymousClient() pmapi.Client
|
||||
}
|
||||
|
||||
type installer interface {
|
||||
InstallUpdate(*semver.Version, io.Reader) error
|
||||
}
|
||||
|
||||
type Updater struct {
|
||||
cm clientProvider
|
||||
installer installer
|
||||
kr *crypto.KeyRing
|
||||
|
||||
curVer *semver.Version
|
||||
updateURLName string
|
||||
platform string
|
||||
rollout float64
|
||||
|
||||
locker *locker
|
||||
}
|
||||
|
||||
func New(
|
||||
cm clientProvider,
|
||||
installer installer,
|
||||
kr *crypto.KeyRing,
|
||||
curVer *semver.Version,
|
||||
updateURLName, platform string,
|
||||
rollout float64,
|
||||
) *Updater {
|
||||
return &Updater{
|
||||
cm: cm,
|
||||
installer: installer,
|
||||
kr: kr,
|
||||
curVer: curVer,
|
||||
updateURLName: updateURLName,
|
||||
platform: platform,
|
||||
rollout: rollout,
|
||||
locker: newLocker(),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Updater) Watch(
|
||||
period time.Duration,
|
||||
handleUpdate func(VersionInfo) error,
|
||||
handleError func(error),
|
||||
) func() {
|
||||
logrus.WithField("period", period).Info("Watching for updates")
|
||||
|
||||
ticker := time.NewTicker(period)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
u.watch(handleUpdate, handleError)
|
||||
<-ticker.C
|
||||
}
|
||||
}()
|
||||
|
||||
return ticker.Stop
|
||||
}
|
||||
|
||||
func (u *Updater) watch(
|
||||
handleUpdate func(VersionInfo) error,
|
||||
handleError func(error),
|
||||
) {
|
||||
logrus.Info("Checking for updates")
|
||||
|
||||
latest, err := u.fetchVersionInfo()
|
||||
if err != nil {
|
||||
handleError(errors.Wrap(err, "failed to fetch version info"))
|
||||
return
|
||||
}
|
||||
|
||||
if !latest.Version.GreaterThan(u.curVer) || u.rollout > latest.Rollout {
|
||||
logrus.WithError(err).Debug("No need to update")
|
||||
return
|
||||
}
|
||||
|
||||
if u.curVer.LessThan(latest.MinAuto) {
|
||||
logrus.Debug("A manual update is required")
|
||||
// NOTE: Need to notify user that they must update manually.
|
||||
return
|
||||
}
|
||||
|
||||
logrus.
|
||||
WithField("latest", latest.Version).
|
||||
WithField("current", u.curVer).
|
||||
Info("An update is available")
|
||||
|
||||
if err := handleUpdate(latest); err != nil {
|
||||
handleError(errors.Wrap(err, "failed to handle update"))
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Updater) InstallUpdate(update VersionInfo) error {
|
||||
return u.locker.doOnce(func() error {
|
||||
logrus.WithField("package", update.Package).Info("Installing update package")
|
||||
|
||||
client := u.cm.GetAnonymousClient()
|
||||
defer client.Logout()
|
||||
|
||||
r, err := client.DownloadAndVerify(update.Package, update.Package+".sig", u.kr)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to download and verify update package")
|
||||
}
|
||||
|
||||
if err := u.installer.InstallUpdate(update.Version, r); err != nil {
|
||||
return errors.Wrap(err, "failed to install update package")
|
||||
}
|
||||
|
||||
u.curVer = update.Version
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (u *Updater) fetchVersionInfo() (VersionInfo, error) {
|
||||
client := u.cm.GetAnonymousClient()
|
||||
defer client.Logout()
|
||||
|
||||
r, err := client.DownloadAndVerify(
|
||||
u.getVersionFileURL(),
|
||||
u.getVersionFileURL()+".sig",
|
||||
u.kr,
|
||||
)
|
||||
if err != nil {
|
||||
return VersionInfo{}, err
|
||||
}
|
||||
|
||||
var versionMap VersionMap
|
||||
|
||||
if err := json.NewDecoder(r).Decode(&versionMap); err != nil {
|
||||
return VersionInfo{}, err
|
||||
}
|
||||
|
||||
return versionMap[Channel], nil
|
||||
}
|
||||
336
internal/updater/updater_test.go
Normal file
336
internal/updater/updater_test.go
Normal file
@ -0,0 +1,336 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
c := gomock.NewController(t)
|
||||
defer c.Finish()
|
||||
|
||||
client := mocks.NewMockClient(c)
|
||||
|
||||
updater := newTestUpdater(client, "1.4.0")
|
||||
|
||||
versionMap := VersionMap{
|
||||
"live": VersionInfo{
|
||||
Version: semver.MustParse("1.5.0"),
|
||||
MinAuto: semver.MustParse("1.4.0"),
|
||||
Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz",
|
||||
Rollout: 1.0,
|
||||
},
|
||||
}
|
||||
|
||||
client.EXPECT().DownloadAndVerify(
|
||||
updater.getVersionFileURL(),
|
||||
updater.getVersionFileURL()+".sig",
|
||||
gomock.Any(),
|
||||
).Return(bytes.NewReader(mustMarshal(t, versionMap)), nil)
|
||||
|
||||
client.EXPECT().Logout()
|
||||
|
||||
updateCh := make(chan VersionInfo)
|
||||
|
||||
defer updater.Watch(
|
||||
time.Minute,
|
||||
func(update VersionInfo) error {
|
||||
updateCh <- update
|
||||
return nil
|
||||
},
|
||||
func(err error) {
|
||||
t.Fatal(err)
|
||||
},
|
||||
)()
|
||||
|
||||
assert.Equal(t, semver.MustParse("1.5.0"), (<-updateCh).Version)
|
||||
}
|
||||
|
||||
func TestWatchIgnoresCurrentVersion(t *testing.T) {
|
||||
c := gomock.NewController(t)
|
||||
defer c.Finish()
|
||||
|
||||
client := mocks.NewMockClient(c)
|
||||
|
||||
updater := newTestUpdater(client, "1.5.0")
|
||||
|
||||
versionMap := VersionMap{
|
||||
"live": VersionInfo{
|
||||
Version: semver.MustParse("1.5.0"),
|
||||
MinAuto: semver.MustParse("1.4.0"),
|
||||
Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz",
|
||||
Rollout: 1.0,
|
||||
},
|
||||
}
|
||||
|
||||
client.EXPECT().DownloadAndVerify(
|
||||
updater.getVersionFileURL(),
|
||||
updater.getVersionFileURL()+".sig",
|
||||
gomock.Any(),
|
||||
).Return(bytes.NewReader(mustMarshal(t, versionMap)), nil)
|
||||
|
||||
client.EXPECT().Logout()
|
||||
|
||||
updateCh := make(chan VersionInfo)
|
||||
|
||||
defer updater.Watch(
|
||||
time.Minute,
|
||||
func(update VersionInfo) error {
|
||||
updateCh <- update
|
||||
return nil
|
||||
},
|
||||
func(err error) {
|
||||
t.Fatal(err)
|
||||
},
|
||||
)()
|
||||
|
||||
select {
|
||||
case <-updateCh:
|
||||
t.Fatal("We shouldn't update because we are already up to date")
|
||||
case <-time.After(1500 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchIgnoresVerionsThatRequireManualUpdate(t *testing.T) {
|
||||
c := gomock.NewController(t)
|
||||
defer c.Finish()
|
||||
|
||||
client := mocks.NewMockClient(c)
|
||||
|
||||
updater := newTestUpdater(client, "1.4.0")
|
||||
|
||||
versionMap := VersionMap{
|
||||
"live": VersionInfo{
|
||||
Version: semver.MustParse("1.5.0"),
|
||||
MinAuto: semver.MustParse("1.5.0"),
|
||||
Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz",
|
||||
Rollout: 1.0,
|
||||
},
|
||||
}
|
||||
|
||||
client.EXPECT().DownloadAndVerify(
|
||||
updater.getVersionFileURL(),
|
||||
updater.getVersionFileURL()+".sig",
|
||||
gomock.Any(),
|
||||
).Return(bytes.NewReader(mustMarshal(t, versionMap)), nil)
|
||||
|
||||
client.EXPECT().Logout()
|
||||
|
||||
updateCh := make(chan VersionInfo)
|
||||
|
||||
defer updater.Watch(
|
||||
time.Minute,
|
||||
func(update VersionInfo) error {
|
||||
updateCh <- update
|
||||
return nil
|
||||
},
|
||||
func(err error) {
|
||||
t.Fatal(err)
|
||||
},
|
||||
)()
|
||||
|
||||
select {
|
||||
case <-updateCh:
|
||||
t.Fatal("We shouldn't update because this version requires a manual update")
|
||||
case <-time.After(1500 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchBadSignature(t *testing.T) {
|
||||
c := gomock.NewController(t)
|
||||
defer c.Finish()
|
||||
|
||||
client := mocks.NewMockClient(c)
|
||||
|
||||
updater := newTestUpdater(client, "1.4.0")
|
||||
|
||||
client.EXPECT().DownloadAndVerify(
|
||||
updater.getVersionFileURL(),
|
||||
updater.getVersionFileURL()+".sig",
|
||||
gomock.Any(),
|
||||
).Return(nil, errors.New("bad signature"))
|
||||
|
||||
client.EXPECT().Logout()
|
||||
|
||||
updateCh := make(chan VersionInfo)
|
||||
errorsCh := make(chan error)
|
||||
|
||||
defer updater.Watch(
|
||||
time.Minute,
|
||||
func(update VersionInfo) error {
|
||||
updateCh <- update
|
||||
return nil
|
||||
},
|
||||
func(err error) {
|
||||
errorsCh <- err
|
||||
},
|
||||
)()
|
||||
|
||||
assert.Error(t, <-errorsCh)
|
||||
}
|
||||
|
||||
func TestInstallUpdate(t *testing.T) {
|
||||
c := gomock.NewController(t)
|
||||
defer c.Finish()
|
||||
|
||||
client := mocks.NewMockClient(c)
|
||||
|
||||
updater := newTestUpdater(client, "1.4.0")
|
||||
|
||||
latestVersion := VersionInfo{
|
||||
Version: semver.MustParse("1.5.0"),
|
||||
MinAuto: semver.MustParse("1.4.0"),
|
||||
Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz",
|
||||
Rollout: 1.0,
|
||||
}
|
||||
|
||||
client.EXPECT().DownloadAndVerify(
|
||||
latestVersion.Package,
|
||||
latestVersion.Package+".sig",
|
||||
gomock.Any(),
|
||||
).Return(bytes.NewReader([]byte("tgz_data_here")), nil)
|
||||
|
||||
client.EXPECT().Logout()
|
||||
|
||||
assert.NoError(t, updater.InstallUpdate(latestVersion))
|
||||
}
|
||||
|
||||
func TestInstallUpdateBadSignature(t *testing.T) {
|
||||
c := gomock.NewController(t)
|
||||
defer c.Finish()
|
||||
|
||||
client := mocks.NewMockClient(c)
|
||||
|
||||
updater := newTestUpdater(client, "1.4.0")
|
||||
|
||||
latestVersion := VersionInfo{
|
||||
Version: semver.MustParse("1.5.0"),
|
||||
MinAuto: semver.MustParse("1.4.0"),
|
||||
Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz",
|
||||
Rollout: 1.0,
|
||||
}
|
||||
|
||||
client.EXPECT().DownloadAndVerify(
|
||||
latestVersion.Package,
|
||||
latestVersion.Package+".sig",
|
||||
gomock.Any(),
|
||||
).Return(nil, errors.New("bad signature"))
|
||||
|
||||
client.EXPECT().Logout()
|
||||
|
||||
assert.Error(t, updater.InstallUpdate(latestVersion))
|
||||
}
|
||||
|
||||
func TestInstallUpdateAlreadyOngoing(t *testing.T) {
|
||||
c := gomock.NewController(t)
|
||||
defer c.Finish()
|
||||
|
||||
client := mocks.NewMockClient(c)
|
||||
|
||||
updater := newTestUpdater(client, "1.4.0")
|
||||
|
||||
updater.installer = &fakeInstaller{delay: 2 * time.Second}
|
||||
|
||||
latestVersion := VersionInfo{
|
||||
Version: semver.MustParse("1.5.0"),
|
||||
MinAuto: semver.MustParse("1.4.0"),
|
||||
Package: "https://protonmail.com/download/bridge/update_1.5.0_linux.tgz",
|
||||
Rollout: 1.0,
|
||||
}
|
||||
|
||||
client.EXPECT().DownloadAndVerify(
|
||||
latestVersion.Package,
|
||||
latestVersion.Package+".sig",
|
||||
gomock.Any(),
|
||||
).Return(bytes.NewReader([]byte("tgz_data_here")), nil)
|
||||
|
||||
client.EXPECT().Logout()
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
assert.NoError(t, updater.InstallUpdate(latestVersion))
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// Wait for the installation to begin.
|
||||
time.Sleep(time.Second)
|
||||
|
||||
err := updater.InstallUpdate(latestVersion)
|
||||
if assert.Error(t, err) {
|
||||
assert.Equal(t, ErrOperationOngoing, err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func newTestUpdater(client *mocks.MockClient, curVer string) *Updater {
|
||||
return New(
|
||||
&fakeClientProvider{client: client},
|
||||
&fakeInstaller{},
|
||||
nil,
|
||||
semver.MustParse(curVer),
|
||||
"bridge", "linux",
|
||||
0.5,
|
||||
)
|
||||
}
|
||||
|
||||
type fakeClientProvider struct {
|
||||
client *mocks.MockClient
|
||||
}
|
||||
|
||||
func (p *fakeClientProvider) GetAnonymousClient() pmapi.Client {
|
||||
return p.client
|
||||
}
|
||||
|
||||
type fakeInstaller struct {
|
||||
bad bool
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func (i *fakeInstaller) InstallUpdate(version *semver.Version, r io.Reader) error {
|
||||
if i.bad {
|
||||
return errors.New("bad install")
|
||||
}
|
||||
|
||||
time.Sleep(i.delay)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustMarshal(t *testing.T, v interface{}) []byte {
|
||||
b, err := json.Marshal(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
return b
|
||||
}
|
||||
85
internal/updater/version.go
Normal file
85
internal/updater/version.go
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
// VersionInfo is information about one version of the app.
|
||||
type VersionInfo struct {
|
||||
// Version is the semantic version of the release.
|
||||
Version *semver.Version
|
||||
|
||||
// MinAuto is the earliest version that is able to autoupdate to this version.
|
||||
// Apps older than this version must run the manual installer and cannot autoupdate.
|
||||
MinAuto *semver.Version
|
||||
|
||||
// Package is the location of the update package.
|
||||
Package string
|
||||
|
||||
// Installers are the locations of installer files (for manual installation).
|
||||
Installers []string
|
||||
|
||||
// Landing is the address of the app landing page on protonmail.com.
|
||||
Landing string
|
||||
|
||||
// Rollout is the current progress of the rollout of this release.
|
||||
Rollout float64
|
||||
}
|
||||
|
||||
// VersionMap represents the structure of the version.json file.
|
||||
// It looks like this:
|
||||
// {
|
||||
// "live": {
|
||||
// "Version": "2.3.4",
|
||||
// "Package": "https://protonmail.com/.../bridge_2.3.4_linux.tgz",
|
||||
// "Installers": [
|
||||
// "https://protonmail.com/.../something.deb",
|
||||
// "https://protonmail.com/.../something.rpm",
|
||||
// "https://protonmail.com/.../PKGBUILD"
|
||||
// ],
|
||||
// "Landing "https://protonmail.com/bridge",
|
||||
// "Rollout": 0.5
|
||||
// },
|
||||
// "beta": {
|
||||
// "Version": "2.4.0-beta",
|
||||
// "Package": "https://protonmail.com/.../bridge_2.4.0-beta_linux.tgz",
|
||||
// "Installers": [
|
||||
// "https://protonmail.com/.../something.deb",
|
||||
// "https://protonmail.com/.../something.rpm",
|
||||
// "https://protonmail.com/.../PKGBUILD"
|
||||
// ],
|
||||
// "Landing "https://protonmail.com/bridge",
|
||||
// "Rollout": 0.5
|
||||
// },
|
||||
// "...": {
|
||||
// ...
|
||||
// }
|
||||
// }
|
||||
type VersionMap map[string]VersionInfo
|
||||
|
||||
// getVersionFileURL returns the URL of the version file.
|
||||
// For example:
|
||||
// - https://protonmail.com/download/bridge/version_linux.json
|
||||
// - https://protonmail.com/download/ie/version_linux.json
|
||||
func (u *Updater) getVersionFileURL() string {
|
||||
return fmt.Sprintf("%v/%v/version_%v.json", Host, u.updateURLName, u.platform)
|
||||
}
|
||||
Reference in New Issue
Block a user