mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-23 10:26:44 +00:00
Import/Export final touches
This commit is contained in:
@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by ./credits.sh at Wed 29 Jul 2020 10:20:09 AM CEST. DO NOT EDIT.
|
||||
// Code generated by ./credits.sh at Wed Aug 12 09:33:24 CEST 2020. DO NOT EDIT.
|
||||
|
||||
package bridge
|
||||
|
||||
const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
const Credits = "github.com/0xAX/notificator;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-delve/delve;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/psampaz/go-mod-outdated;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
|
||||
@ -24,12 +24,23 @@ import (
|
||||
"runtime/pprof"
|
||||
)
|
||||
|
||||
// StartCPUProfile starts CPU pprof.
|
||||
func StartCPUProfile() {
|
||||
f, err := os.Create("./cpu.pprof")
|
||||
if err != nil {
|
||||
log.Fatal("Could not create CPU profile: ", err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatal("Could not start CPU profile: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// MakeMemoryProfile generates memory pprof.
|
||||
func MakeMemoryProfile() {
|
||||
name := "./mem.pprof"
|
||||
f, err := os.Create(name)
|
||||
if err != nil {
|
||||
log.Error("Could not create memory profile: ", err)
|
||||
log.Fatal("Could not create memory profile: ", err)
|
||||
}
|
||||
if abs, err := filepath.Abs(name); err == nil {
|
||||
name = abs
|
||||
@ -37,7 +48,7 @@ func MakeMemoryProfile() {
|
||||
log.Info("Writing memory profile to ", name)
|
||||
runtime.GC() // get up-to-date statistics
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
log.Error("Could not write memory profile: ", err)
|
||||
log.Fatal("Could not write memory profile: ", err)
|
||||
}
|
||||
_ = f.Close()
|
||||
}
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
package cmd
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/pkg/updates"
|
||||
import "github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
|
||||
// GenerateVersionFiles writes a JSON file with details about current build.
|
||||
// Those files are used for upgrading the app.
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// 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 cli
|
||||
package cliie
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// 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 cli
|
||||
package cliie
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
// 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 cli provides CLI interface of the Import/Export.
|
||||
package cli
|
||||
// Package cliie provides CLI interface of the Import-Export app.
|
||||
package cliie
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
@ -68,7 +68,7 @@ func New( //nolint[funlen]
|
||||
Aliases: []string{"cl"},
|
||||
}
|
||||
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
|
||||
Help: "remove all accounts from keychain. (aliases: k, keychain)",
|
||||
Help: "remove all accounts from keychain. (aliases: a, k, keychain)",
|
||||
Aliases: []string{"a", "k", "keychain"},
|
||||
Func: fe.deleteAccounts,
|
||||
})
|
||||
@ -77,7 +77,7 @@ func New( //nolint[funlen]
|
||||
// Check commands.
|
||||
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."}
|
||||
checkCmd.AddCmd(&ishell.Cmd{Name: "updates",
|
||||
Help: "check for Import/Export updates. (aliases: u, v, version)",
|
||||
Help: "check for Import-Export updates. (aliases: u, v, version)",
|
||||
Aliases: []string{"u", "version", "v"},
|
||||
Func: fe.checkUpdates,
|
||||
})
|
||||
@ -134,7 +134,7 @@ func New( //nolint[funlen]
|
||||
Completer: fe.completeUsernames,
|
||||
})
|
||||
|
||||
// Import/Export commands.
|
||||
// Import-Export commands.
|
||||
importCmd := &ishell.Cmd{Name: "import",
|
||||
Help: "import messages. (alias: imp)",
|
||||
Aliases: []string{"imp"},
|
||||
@ -167,7 +167,7 @@ func New( //nolint[funlen]
|
||||
|
||||
// System commands.
|
||||
fe.AddCmd(&ishell.Cmd{Name: "restart",
|
||||
Help: "restart the import/export.",
|
||||
Help: "restart the Import-Export app.",
|
||||
Func: fe.restart,
|
||||
})
|
||||
|
||||
@ -190,7 +190,7 @@ func (f *frontendCLI) watchEvents() {
|
||||
for {
|
||||
select {
|
||||
case errorDetails := <-errorCh:
|
||||
f.Println("Import/Export failed:", errorDetails)
|
||||
f.Println("Import-Export failed:", errorDetails)
|
||||
case <-internetOffCh:
|
||||
f.notifyInternetOff()
|
||||
case <-internetOnCh:
|
||||
@ -228,9 +228,9 @@ func (f *frontendCLI) Loop(credentialsError error) error {
|
||||
}
|
||||
|
||||
f.Print(`
|
||||
Welcome to ProtonMail Import/Export interactive shell
|
||||
Welcome to ProtonMail Import-Export interactive shell
|
||||
|
||||
WARNING: CLI is experimental feature and does not cover all functionality yet.
|
||||
WARNING: The CLI is an experimental feature and does not yet cover all functionality.
|
||||
`)
|
||||
f.Run()
|
||||
return nil
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// 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 cli
|
||||
package cliie
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
@ -15,19 +15,15 @@
|
||||
// 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 cli
|
||||
package cliie
|
||||
|
||||
import (
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
var (
|
||||
currentPort = "" //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
func (f *frontendCLI) restart(c *ishell.Context) {
|
||||
if f.yesNoQuestion("Are you sure you want to restart the Import/Export") {
|
||||
f.Println("Restarting Import/Export...")
|
||||
if f.yesNoQuestion("Are you sure you want to restart the Import-Export") {
|
||||
f.Println("Restarting the Import-Export app...")
|
||||
f.appRestart = true
|
||||
f.Stop()
|
||||
}
|
||||
@ -37,7 +33,7 @@ func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
|
||||
if f.ie.CheckConnection() == nil {
|
||||
f.Println("Internet connection is available.")
|
||||
} else {
|
||||
f.Println("Can not contact the server, please check you internet connection.")
|
||||
f.Println("Can not contact the server, please check your internet connection.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,5 +42,5 @@ func (f *frontendCLI) printLogDir(c *ishell.Context) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printManual(c *ishell.Context) {
|
||||
f.Println("More instructions about the Import/Export can be found at\n\n https://protonmail.com/support/categories/import-export/")
|
||||
f.Println("More instructions about the Import-Export app can be found at\n\n https://protonmail.com/support/categories/import-export/")
|
||||
}
|
||||
|
||||
@ -15,13 +15,13 @@
|
||||
// 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 cli
|
||||
package cliie
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/updates"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
@ -47,7 +47,7 @@ func (f *frontendCLI) printLocalReleaseNotes(c *ishell.Context) {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printReleaseNotes(versionInfo updates.VersionInfo) {
|
||||
f.Println(bold("ProtonMail Import/Export "+versionInfo.Version), "\n")
|
||||
f.Println(bold("ProtonMail Import-Export "+versionInfo.Version), "\n")
|
||||
if versionInfo.ReleaseNotes != "" {
|
||||
f.Println(bold("Release Notes"))
|
||||
f.Println(versionInfo.ReleaseNotes)
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// 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 cli
|
||||
package cliie
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@ -98,7 +98,7 @@ func (f *frontendCLI) notifyNeedUpgrade() {
|
||||
|
||||
func (f *frontendCLI) notifyCredentialsError() {
|
||||
// Print in 80-column width.
|
||||
f.Println("ProtonMail Import/Export is not able to detect a supported password manager")
|
||||
f.Println("ProtonMail Import-Export is not able to detect a supported password manager")
|
||||
f.Println("(pass, gnome-keyring). Please install and set up a supported password manager")
|
||||
f.Println("and restart the application.")
|
||||
}
|
||||
@ -109,7 +109,7 @@ func (f *frontendCLI) notifyCertIssue() {
|
||||
be insecure.
|
||||
|
||||
Description:
|
||||
ProtonMail Import/Export was not able to establish a secure connection to Proton
|
||||
ProtonMail Import-Export was not able to establish a secure connection to Proton
|
||||
servers due to a TLS certificate error. This means your connection may
|
||||
potentially be insecure and susceptible to monitoring by third parties.
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@ func New( //nolint[funlen]
|
||||
Func: fe.deleteCache,
|
||||
})
|
||||
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
|
||||
Help: "remove all accounts from keychain. (aliases: k, keychain)",
|
||||
Help: "remove all accounts from keychain. (aliases: a, k, keychain)",
|
||||
Aliases: []string{"a", "k", "keychain"},
|
||||
Func: fe.deleteAccounts,
|
||||
})
|
||||
|
||||
@ -43,7 +43,7 @@ func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
|
||||
if f.bridge.CheckConnection() == nil {
|
||||
f.Println("Internet connection is available.")
|
||||
} else {
|
||||
f.Println("Can not contact the server, please check you internet connection.")
|
||||
f.Println("Can not contact the server, please check your internet connection.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/updates"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/abiosoft/ishell"
|
||||
)
|
||||
|
||||
|
||||
@ -458,7 +458,7 @@ Dialog {
|
||||
if (progressbarImport.isFinished) return qsTr("Import finished","todo")
|
||||
if (
|
||||
go.progressDescription == gui.enums.progressInit ||
|
||||
(go.progress == 0 && go.description=="")
|
||||
(go.progress == 0 && go.progressDescription=="")
|
||||
) return qsTr("Estimating the total number of messages","todo")
|
||||
if (
|
||||
go.progressDescription == gui.enums.progressLooping
|
||||
|
||||
@ -43,6 +43,8 @@ Rectangle {
|
||||
property string lastTargetFolder: "6" // Archive
|
||||
property string lastTargetLabels: "" // no flag by default
|
||||
|
||||
property string sourceID : mboxID
|
||||
property string sourceName : name
|
||||
|
||||
Rectangle {
|
||||
id: line
|
||||
@ -71,7 +73,7 @@ Rectangle {
|
||||
|
||||
Text {
|
||||
id: folderIcon
|
||||
text : gui.folderIcon(name, gui.enums.folderTypeFolder)
|
||||
text : gui.folderIcon(root.sourceName, gui.enums.folderTypeFolder)
|
||||
anchors.verticalCenter : parent.verticalCenter
|
||||
color: root.isSourceSelected ? Style.main.text : Style.main.textDisabled
|
||||
font {
|
||||
@ -81,7 +83,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
Text {
|
||||
text : name
|
||||
text : root.sourceName
|
||||
width: nameWidth
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter : parent.verticalCenter
|
||||
@ -102,8 +104,8 @@ Rectangle {
|
||||
|
||||
SelectFolderMenu {
|
||||
id: selectFolder
|
||||
sourceID: mboxID
|
||||
targets: transferRules.targetFolders(mboxID)
|
||||
sourceID: root.sourceID
|
||||
targets: transferRules.targetFolders(root.sourceID)
|
||||
width: nameWidth
|
||||
anchors.verticalCenter : parent.verticalCenter
|
||||
enabled: root.isSourceSelected
|
||||
@ -112,8 +114,8 @@ Rectangle {
|
||||
}
|
||||
|
||||
SelectLabelsMenu {
|
||||
sourceID: mboxID
|
||||
targets: transferRules.targetLabels(mboxID)
|
||||
sourceID: root.sourceID
|
||||
targets: transferRules.targetLabels(root.sourceID)
|
||||
width: nameWidth
|
||||
anchors.verticalCenter : parent.verticalCenter
|
||||
enabled: root.isSourceSelected
|
||||
@ -130,7 +132,7 @@ Rectangle {
|
||||
|
||||
DateRangeMenu {
|
||||
id: dateRangeMenu
|
||||
sourceID: mboxID
|
||||
sourceID: root.sourceID
|
||||
sourceFromDate: fromDate
|
||||
sourceToDate: toDate
|
||||
|
||||
@ -143,10 +145,10 @@ Rectangle {
|
||||
|
||||
|
||||
function importToFolder(newTargetID) {
|
||||
transferRules.addTargetID(mboxID,newTargetID)
|
||||
transferRules.addTargetID(root.sourceID,newTargetID)
|
||||
}
|
||||
|
||||
function toggleImport() {
|
||||
transferRules.setIsRuleActive(mboxID, !root.isSourceSelected)
|
||||
transferRules.setIsRuleActive(root.sourceID, !root.isSourceSelected)
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@ Rectangle {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: {
|
||||
if (listview.currentIndex<0) return 0
|
||||
if (listview.currentItem === null) return 0
|
||||
else return listview.currentItem.leftMargin1
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@ Window {
|
||||
rightMargin: innerWindowBorder
|
||||
}
|
||||
model: [
|
||||
{ "title" : qsTr("Import/Export" , "title of tab that shows account list" ), "iconText": Style.fa.home },
|
||||
{ "title" : qsTr("Import-Export" , "title of tab that shows account list" ), "iconText": Style.fa.home },
|
||||
{ "title" : qsTr("Settings" , "title of tab that allows user to change settings" ), "iconText": Style.fa.cogs },
|
||||
{ "title" : qsTr("Help" , "title of tab that shows the help menu" ), "iconText": Style.fa.life_ring }
|
||||
]
|
||||
@ -381,8 +381,9 @@ Window {
|
||||
|
||||
onClickedNo: popupMessage.hide()
|
||||
onClickedOkay: popupMessage.hide()
|
||||
onClickedCancel: popupMessage.hide()
|
||||
onClickedYes: {
|
||||
if (popupMessage.message == gui.areYouSureYouWantToQuit) Qt.quit()
|
||||
if (popupMessage.text == gui.areYouSureYouWantToQuit) Qt.quit()
|
||||
}
|
||||
}
|
||||
|
||||
@ -461,8 +462,9 @@ Window {
|
||||
(dialogExport.visible && dialogExport.currentIndex == 2 && go.progress!=1)
|
||||
) {
|
||||
popupMessage.buttonOkay .visible = false
|
||||
popupMessage.buttonNo .visible = true
|
||||
popupMessage.buttonYes .visible = true
|
||||
popupMessage.buttonYes .visible = false
|
||||
popupMessage.buttonQuit .visible = true
|
||||
popupMessage.buttonCancel .visible = true
|
||||
popupMessage.show ( gui.areYouSureYouWantToQuit )
|
||||
return
|
||||
}
|
||||
|
||||
@ -58,7 +58,6 @@ ComboBox {
|
||||
}
|
||||
|
||||
displayText: {
|
||||
console.log("Target Menu current", view.currentItem, view.currentIndex)
|
||||
if (view.currentIndex >= 0) {
|
||||
if (!root.isFolderType) return Style.fa.tags + " " + qsTr("Add/Remove labels")
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ Rectangle {
|
||||
color: Style.transparent
|
||||
property alias text : message.text
|
||||
property alias checkbox : checkbox
|
||||
property alias buttonQuit : buttonQuit
|
||||
property alias buttonOkay : buttonOkay
|
||||
property alias buttonYes : buttonYes
|
||||
property alias buttonNo : buttonNo
|
||||
@ -89,13 +90,13 @@ Rectangle {
|
||||
spacing: Style.dialog.spacing
|
||||
anchors.horizontalCenter : parent.horizontalCenter
|
||||
|
||||
ButtonRounded { id : buttonQuit ; text : qsTr ( "Stop & quit", "" ) ; onClicked : root.clickedYes ( ) ; visible : false ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
|
||||
ButtonRounded { id : buttonNo ; text : qsTr ( "No" , "Button No" ) ; onClicked : root.clickedNo ( ) ; visible : false ; isOpaque : false ; color_main : Style.errorDialog.text ; color_minor : Style.transparent ; }
|
||||
ButtonRounded { id : buttonYes ; text : qsTr ( "Yes" , "Button Yes" ) ; onClicked : root.clickedYes ( ) ; visible : false ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
|
||||
ButtonRounded { id : buttonRetry ; text : qsTr ( "Retry" , "Button Retry" ) ; onClicked : root.clickedRetry ( ) ; visible : false ; isOpaque : false ; color_main : Style.errorDialog.text ; color_minor : Style.transparent ; }
|
||||
ButtonRounded { id : buttonSkip ; text : qsTr ( "Skip" , "Button Skip" ) ; onClicked : root.clickedSkip ( ) ; visible : false ; isOpaque : false ; color_main : Style.errorDialog.text ; color_minor : Style.transparent ; }
|
||||
ButtonRounded { id : buttonCancel ; text : qsTr ( "Cancel" , "Button Cancel" ) ; onClicked : root.clickedCancel ( ) ; visible : false ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
|
||||
ButtonRounded { id : buttonOkay ; text : qsTr ( "Okay" , "Button Okay" ) ; onClicked : root.clickedOkay ( ) ; visible : true ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ Window {
|
||||
id: buttons
|
||||
|
||||
ListElement { title : "Show window" }
|
||||
ListElement { title : "Logout cuthix" }
|
||||
ListElement { title : "Logout" }
|
||||
ListElement { title : "Internet on" }
|
||||
ListElement { title : "Internet off" }
|
||||
ListElement { title : "Macos" }
|
||||
@ -143,8 +143,8 @@ Window {
|
||||
case "Show window" :
|
||||
go.showWindow();
|
||||
break;
|
||||
case "Logout cuthix" :
|
||||
go.checkLoggedOut("cuthix");
|
||||
case "Logout" :
|
||||
go.checkLoggedOut("ie");
|
||||
break;
|
||||
case "Internet on" :
|
||||
go.setConnectionStatus(true);
|
||||
@ -223,10 +223,10 @@ Window {
|
||||
|
||||
ListModel{
|
||||
id: accountsModel
|
||||
ListElement{ account : "cuthix" ; status : "connected"; isExpanded: false; isCombinedAddressMode: false; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "cuto@pm.com;jaku@pm.com;DoYouKnowAboutAMovieCalledTheHorriblySlowMurderWithExtremelyInefficientWeapon@thatYouCanFindForExampleOnyoutube.com" }
|
||||
ListElement{ account : "exteremelongnamewhichmustbeeladedinthemiddleoftheaddress@protonmail.com" ; status : "connected"; isExpanded: true; isCombinedAddressMode: true; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "cuto@pm.com;jaku@pm.com;hu@hu.hu" }
|
||||
ListElement{ account : "cuthix2@protonmail.com" ; status : "disconnected"; isExpanded: false; isCombinedAddressMode: false; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "cuto@pm.com;jaku@pm.com;hu@hu.hu" }
|
||||
ListElement{ account : "many@protonmail.com" ; status : "connected"; isExpanded: true; isCombinedAddressMode: true; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;cuto@pm.com;jaku@pm.com;hu@hu.hu;"}
|
||||
ListElement{ account : "ie" ; status : "connected"; isExpanded: false; isCombinedAddressMode: false; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "ie@pm.com;jaku@pm.com;DoYouKnowAboutAMovieCalledTheHorriblySlowMurderWithExtremelyInefficientWeapon@thatYouCanFindForExampleOnyoutube.com" }
|
||||
ListElement{ account : "exteremelongnamewhichmustbeeladedinthemiddleoftheaddress@protonmail.com" ; status : "connected"; isExpanded: true; isCombinedAddressMode: true; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "ie@pm.com;jaku@pm.com;hu@hu.hu" }
|
||||
ListElement{ account : "ie2@protonmail.com" ; status : "disconnected"; isExpanded: false; isCombinedAddressMode: false; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "ie@pm.com;jaku@pm.com;hu@hu.hu" }
|
||||
ListElement{ account : "many@protonmail.com" ; status : "connected"; isExpanded: true; isCombinedAddressMode: true; hostname : "127.0.0.1"; password : "ZI9tKp+ryaxmbpn2E12"; security : "StarTLS"; portSMTP : 1025; portIMAP : 1143; aliases : "ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;ie@pm.com;jaku@pm.com;hu@hu.hu;"}
|
||||
}
|
||||
|
||||
ListModel{
|
||||
@ -830,9 +830,9 @@ Window {
|
||||
property string bugNotSent
|
||||
property string bugReportSent
|
||||
|
||||
property string programTitle : "ProtonMail Import/Export Tool"
|
||||
property string programTitle : "ProtonMail Import-Export App"
|
||||
property string newversion : "q0.1.0"
|
||||
property string landingPage : "https://jakub.cuth.sk/bridge"
|
||||
property string landingPage : "https://landing.page"
|
||||
property string changelog : "• Lorem ipsum dolor sit amet\n• consetetur sadipscing elitr,\n• sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,\n• sed diam voluptua.\n• At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
|
||||
//property string changelog : ""
|
||||
property string bugfixes : "• lorem ipsum dolor sit amet;• consetetur sadipscing elitr;• sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat;• sed diam voluptua;• at vero eos et accusam et justo duo dolores et ea rebum;• stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet"
|
||||
|
||||
@ -103,16 +103,6 @@ func PauseLong() {
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
|
||||
func ParsePMAPIError(err error, code int) error {
|
||||
/*
|
||||
if err == pmapi.ErrAPINotReachable {
|
||||
code = ErrNoInternet
|
||||
}
|
||||
return errors.NewFromError(code, err)
|
||||
*/
|
||||
return nil
|
||||
}
|
||||
|
||||
// FIXME: Not working in test...
|
||||
func WaitForEnter() {
|
||||
log.Print("Press 'Enter' to continue...")
|
||||
|
||||
@ -47,6 +47,18 @@ func (f *FrontendQt) LoadStructureForExport(addressOrID string) {
|
||||
return
|
||||
}
|
||||
|
||||
// Export has only one option to set time limits--by global time range.
|
||||
// In case user changes file or because of some bug global time is saved
|
||||
// to all rules, let's clear it, because there is no way to show it in
|
||||
// GUI and user would be confused and see it does not work at all.
|
||||
for _, rule := range f.transfer.GetRules() {
|
||||
isActive := rule.Active
|
||||
f.transfer.SetRule(rule.SourceMailbox, rule.TargetMailboxes, 0, 0)
|
||||
if !isActive {
|
||||
f.transfer.UnsetRule(rule.SourceMailbox)
|
||||
}
|
||||
}
|
||||
|
||||
f.TransferRules.setTransfer(f.transfer)
|
||||
}
|
||||
|
||||
@ -65,55 +77,4 @@ func (f *FrontendQt) StartExport(rootPath, login, fileType string, attachEncrypt
|
||||
f.transfer.SetSkipEncryptedMessages(!attachEncryptedBody)
|
||||
progress := f.transfer.Start()
|
||||
f.setProgressManager(progress)
|
||||
|
||||
/*
|
||||
TODO
|
||||
f.Qml.SetProgress(0.0)
|
||||
f.Qml.SetProgressDescription(backend.ProgressInit)
|
||||
f.Qml.SetTotal(0)
|
||||
|
||||
settings := backend.ExportSettings{
|
||||
FilePath: fpath,
|
||||
Login: login,
|
||||
AttachEncryptedBody: attachEncryptedBody,
|
||||
DateBegin: 0,
|
||||
DateEnd: 0,
|
||||
Labels: make(map[string]string),
|
||||
}
|
||||
|
||||
if fileType == "EML" {
|
||||
settings.FileTypeID = backend.EMLFormat
|
||||
} else if fileType == "MBOX" {
|
||||
settings.FileTypeID = backend.MBOXFormat
|
||||
} else {
|
||||
log.Errorln("Wrong file format:", fileType)
|
||||
return
|
||||
}
|
||||
|
||||
username, _, err := backend.ExtractUsername(login)
|
||||
if err != nil {
|
||||
log.Error("qtfrontend: cannot retrieve username from alias: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
settings.User, err = backend.ExtractCurrentUser(username)
|
||||
if err != nil && !errors.IsCode(err, errors.ErrUnlockUser) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, entity := range f.PMStructure.entities {
|
||||
if entity.IsFolderSelected {
|
||||
settings.Labels[entity.FolderName] = entity.FolderId
|
||||
}
|
||||
}
|
||||
|
||||
settings.DateBegin = f.PMStructure.GlobalOptions.FromDate
|
||||
settings.DateEnd = f.PMStructure.GlobalOptions.ToDate
|
||||
|
||||
settings.PM = backend.NewProcessManager()
|
||||
f.setHandlers(settings.PM)
|
||||
|
||||
log.Debugln("start export", settings.FilePath)
|
||||
go backend.Export(f.panicHandler, settings)
|
||||
*/
|
||||
}
|
||||
|
||||
@ -59,10 +59,6 @@ func getTargetHashes(mboxes []transfer.Mailbox) (targetFolderID, targetLabelIDs
|
||||
return
|
||||
}
|
||||
|
||||
func isSystemMailbox(mbox transfer.Mailbox) bool {
|
||||
return pmapi.IsSystemLabel(mbox.ID)
|
||||
}
|
||||
|
||||
func newFolderInfo(mbox transfer.Mailbox, rule *transfer.Rule) *FolderInfo {
|
||||
targetFolderID, targetLabelIDs := getTargetHashes(rule.TargetMailboxes)
|
||||
|
||||
@ -77,7 +73,7 @@ func newFolderInfo(mbox transfer.Mailbox, rule *transfer.Rule) *FolderInfo {
|
||||
}
|
||||
|
||||
entry.FolderType = FolderTypeSystem
|
||||
if !isSystemMailbox(mbox) {
|
||||
if !pmapi.IsSystemLabel(mbox.ID) {
|
||||
if mbox.IsExclusive {
|
||||
entry.FolderType = FolderTypeFolder
|
||||
} else {
|
||||
@ -112,7 +108,7 @@ func (s *FolderStructure) saveRule(info *FolderInfo) error {
|
||||
return s.transfer.SetRule(sourceMbox, targetMboxes, info.FromDate, info.ToDate)
|
||||
}
|
||||
|
||||
func (s *FolderInfo) updateTgtLblIDs(targetLabelsSet map[string]struct{}) {
|
||||
func (s *FolderInfo) updateTargetLabelIDs(targetLabelsSet map[string]struct{}) {
|
||||
targets := []string{}
|
||||
for key := range targetLabelsSet {
|
||||
targets = append(targets, key)
|
||||
@ -120,17 +116,13 @@ func (s *FolderInfo) updateTgtLblIDs(targetLabelsSet map[string]struct{}) {
|
||||
s.TargetLabelIDs = strings.Join(targets, ";")
|
||||
}
|
||||
|
||||
func (s *FolderInfo) clearTgtLblIDs() {
|
||||
s.TargetLabelIDs = ""
|
||||
}
|
||||
|
||||
func (s *FolderInfo) AddTargetLabel(targetID string) {
|
||||
if targetID == "" {
|
||||
return
|
||||
}
|
||||
targetLabelsSet := s.getSetOfLabels()
|
||||
targetLabelsSet[targetID] = struct{}{}
|
||||
s.updateTgtLblIDs(targetLabelsSet)
|
||||
s.updateTargetLabelIDs(targetLabelsSet)
|
||||
}
|
||||
|
||||
func (s *FolderInfo) RemoveTargetLabel(targetID string) {
|
||||
@ -139,7 +131,7 @@ func (s *FolderInfo) RemoveTargetLabel(targetID string) {
|
||||
}
|
||||
targetLabelsSet := s.getSetOfLabels()
|
||||
delete(targetLabelsSet, targetID)
|
||||
s.updateTgtLblIDs(targetLabelsSet)
|
||||
s.updateTargetLabelIDs(targetLabelsSet)
|
||||
}
|
||||
|
||||
func (s *FolderInfo) IsType(askType string) bool {
|
||||
@ -387,7 +379,7 @@ func (s *FolderStructure) setTargetFolderID(id, target string) {
|
||||
s.changedEntityRole(i, i, TargetFolderID)
|
||||
if target == "" { // do not import
|
||||
before := info.TargetLabelIDs
|
||||
info.clearTgtLblIDs()
|
||||
info.TargetLabelIDs = ""
|
||||
if err := s.saveRule(info); err != nil {
|
||||
info.TargetLabelIDs = before
|
||||
log.WithError(err).WithField("id", id).WithField("target", target).Error("Cannot set target")
|
||||
|
||||
@ -29,9 +29,9 @@ import (
|
||||
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/transfer"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/updates"
|
||||
|
||||
"github.com/therecipe/qt/core"
|
||||
"github.com/therecipe/qt/gui"
|
||||
@ -185,7 +185,7 @@ func (f *FrontendQt) qtSetupQmlAndStructures() {
|
||||
f.View.Load(core.NewQUrl3("qrc:/uiie.qml", 0))
|
||||
|
||||
// TODO set the first start flag
|
||||
log.Error("Get FirstStart: Not implemented")
|
||||
//log.Error("Get FirstStart: Not implemented")
|
||||
//if prefs.Get(prefs.FirstStart) == "true" {
|
||||
if false {
|
||||
f.Qml.SetIsFirstStart(true)
|
||||
@ -226,7 +226,6 @@ func (f *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error {
|
||||
return err
|
||||
}
|
||||
log.Debug("Closing...")
|
||||
log.Error("Set FirstStart: Not implemented")
|
||||
//prefs.Set(prefs.FirstStart, "false")
|
||||
return nil
|
||||
}
|
||||
@ -318,27 +317,31 @@ func (f *FrontendQt) setProgressManager(progress *transfer.Progress) {
|
||||
f.Qml.ConnectCancelProcess(func() {
|
||||
progress.Stop()
|
||||
})
|
||||
f.Qml.SetProgress(0)
|
||||
|
||||
go func() {
|
||||
log.Trace("Start reading updates")
|
||||
defer func() {
|
||||
log.Trace("Finishing reading updates")
|
||||
f.Qml.DisconnectPauseProcess()
|
||||
f.Qml.DisconnectResumeProcess()
|
||||
f.Qml.DisconnectCancelProcess()
|
||||
f.Qml.SetProgress(1)
|
||||
f.progress = nil
|
||||
f.ErrorList.Progress = nil
|
||||
}()
|
||||
|
||||
//TODO get log file (in old code it was here, but this is ugly place probably somewhere else)
|
||||
updates := progress.GetUpdateChannel()
|
||||
for range updates {
|
||||
if progress.IsStopped() {
|
||||
break
|
||||
}
|
||||
failed, imported, _, _, total := progress.GetCounts()
|
||||
if total != 0 { // udate total
|
||||
if total != 0 {
|
||||
f.Qml.SetTotal(int(total))
|
||||
}
|
||||
f.Qml.SetProgressFails(int(failed))
|
||||
f.Qml.SetProgressDescription(progress.PauseReason()) // TODO add description when changing folders?
|
||||
f.Qml.SetProgressDescription(progress.PauseReason())
|
||||
if total > 0 {
|
||||
newProgress := float32(imported+failed) / float32(total)
|
||||
if newProgress >= 0 && newProgress != f.Qml.Progress() {
|
||||
@ -436,7 +439,7 @@ func (f *FrontendQt) getLocalVersionInfo() {
|
||||
// LeastUsedColor is intended to return color for creating a new inbox or label.
|
||||
func (f *FrontendQt) leastUsedColor() string {
|
||||
if f.transfer == nil {
|
||||
log.Errorln("Getting least used color before transfer exist.")
|
||||
log.Warnln("Getting least used color before transfer exist.")
|
||||
return "#7272a7"
|
||||
}
|
||||
|
||||
|
||||
@ -74,6 +74,8 @@ func (f *FrontendQt) loadStructuresForImport() error {
|
||||
}
|
||||
|
||||
func (f *FrontendQt) StartImport(email string) { // TODO email not needed
|
||||
log.Trace("Starting import")
|
||||
|
||||
f.Qml.SetProgressDescription("init") // TODO use const
|
||||
f.Qml.SetProgressFails(0)
|
||||
f.Qml.SetProgress(0.0)
|
||||
|
||||
@ -55,8 +55,8 @@ func newMboxList(t *TransferRules, rule *transfer.Rule, containsFolders bool) *M
|
||||
m.log = log.
|
||||
WithField("rule", m.rule.SourceMailbox.Hash()).
|
||||
WithField("folders", m.containsFolders)
|
||||
m.updateSelectedIndex()
|
||||
m.EndResetModel()
|
||||
m.itemsChanged(rule)
|
||||
return m
|
||||
}
|
||||
|
||||
@ -71,11 +71,6 @@ func (m *MboxList) rowCount(index *core.QModelIndex) int {
|
||||
}
|
||||
|
||||
func (m *MboxList) roleNames() map[int]*core.QByteArray {
|
||||
m.log.
|
||||
WithField("isActive", MboxIsActive).
|
||||
WithField("id", MboxID).
|
||||
WithField("color", MboxColor).
|
||||
Debug("role names")
|
||||
return map[int]*core.QByteArray{
|
||||
MboxIsActive: qtcommon.NewQByteArrayFromString("isActive"),
|
||||
MboxID: qtcommon.NewQByteArrayFromString("mboxID"),
|
||||
@ -88,17 +83,17 @@ func (m *MboxList) roleNames() map[int]*core.QByteArray {
|
||||
func (m *MboxList) data(index *core.QModelIndex, role int) *core.QVariant {
|
||||
allTargets := m.targetMailboxes()
|
||||
|
||||
i, valid := index.Row(), index.IsValid()
|
||||
l := m.log.WithField("row", i).WithField("role", role)
|
||||
l.Trace("called data()")
|
||||
i := index.Row()
|
||||
log := m.log.WithField("row", i).WithField("role", role)
|
||||
log.Trace("Mbox data")
|
||||
|
||||
if !valid || i >= len(allTargets) {
|
||||
l.WithField("row", i).Warning("Invalid index")
|
||||
if i >= len(allTargets) {
|
||||
log.Warning("Invalid index")
|
||||
return core.NewQVariant()
|
||||
}
|
||||
|
||||
if m.transfer == nil {
|
||||
l.Warning("Requested mbox list data before transfer is connected")
|
||||
log.Warning("Requested mbox list data before transfer is connected")
|
||||
return qtcommon.NewQVariantString("")
|
||||
}
|
||||
|
||||
@ -131,7 +126,7 @@ func (m *MboxList) data(index *core.QModelIndex, role int) *core.QVariant {
|
||||
return qtcommon.NewQVariantString(mbox.Color)
|
||||
|
||||
default:
|
||||
l.Error("Requested mbox list data with unknown role")
|
||||
log.Error("Requested mbox list data with unknown role")
|
||||
return qtcommon.NewQVariantString("")
|
||||
}
|
||||
}
|
||||
@ -161,11 +156,10 @@ func (m *MboxList) filter(mailboxes []transfer.Mailbox) (filtered []transfer.Mai
|
||||
func (m *MboxList) itemsChanged(rule *transfer.Rule) {
|
||||
m.rule = rule
|
||||
allTargets := m.targetMailboxes()
|
||||
l := m.log.WithField("count", len(allTargets))
|
||||
l.Trace("called itemChanged()")
|
||||
defer func() {
|
||||
l.WithField("selected", m.SelectedIndex()).Trace("index updated")
|
||||
}()
|
||||
|
||||
m.log.WithField("count", len(allTargets)).Trace("Mbox items changed")
|
||||
|
||||
m.updateSelectedIndex()
|
||||
|
||||
// NOTE: Be careful with indices: If they are invalid the DataChanged
|
||||
// signal will not be sent to QML e.g. `end == rowCount - 1`
|
||||
@ -175,7 +169,10 @@ func (m *MboxList) itemsChanged(rule *transfer.Rule) {
|
||||
changedRoles := []int{MboxIsActive}
|
||||
m.DataChanged(begin, end, changedRoles)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MboxList) updateSelectedIndex() {
|
||||
allTargets := m.targetMailboxes()
|
||||
for index, targetMailbox := range allTargets {
|
||||
for _, selectedTarget := range m.rule.TargetMailboxes {
|
||||
if targetMailbox.Hash() == selectedTarget.Hash() {
|
||||
|
||||
@ -15,7 +15,9 @@
|
||||
// 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 qtcommon
|
||||
// +build !nogui
|
||||
|
||||
package qtie
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
@ -44,6 +44,7 @@ type TransferRules struct {
|
||||
_ func(sourceID string, targetID string) `slot:"addTargetID,auto"`
|
||||
_ func(sourceID string, targetID string) `slot:"removeTargetID,auto"`
|
||||
|
||||
// globalFromDate and globalToDate is just default value for GUI, always zero.
|
||||
_ int `property:"globalFromDate"`
|
||||
_ int `property:"globalToDate"`
|
||||
_ bool `property:"isLabelGroupSelected"`
|
||||
@ -90,21 +91,23 @@ func (t *TransferRules) roleNames() map[int]*core.QByteArray {
|
||||
}
|
||||
|
||||
func (t *TransferRules) data(index *core.QModelIndex, role int) *core.QVariant {
|
||||
i, valid := index.Row(), index.IsValid()
|
||||
|
||||
if !valid || i >= t.rowCount(index) {
|
||||
log.WithField("row", i).Warning("Invalid index")
|
||||
return core.NewQVariant()
|
||||
}
|
||||
i := index.Row()
|
||||
allRules := t.transfer.GetRules()
|
||||
|
||||
log := log.WithField("row", i).WithField("role", role)
|
||||
log.Trace("Transfer rules data")
|
||||
|
||||
if i >= len(allRules) {
|
||||
log.Warning("Invalid index")
|
||||
return core.NewQVariant()
|
||||
}
|
||||
|
||||
if t.transfer == nil {
|
||||
log.Warning("Requested transfer rules data before transfer is connected")
|
||||
return qtcommon.NewQVariantString("")
|
||||
}
|
||||
|
||||
rule := t.transfer.GetRules()[i]
|
||||
rule := allRules[i]
|
||||
|
||||
switch role {
|
||||
case MboxIsActive:
|
||||
@ -160,6 +163,9 @@ func (t *TransferRules) setTransfer(transfer *transfer.Transfer) {
|
||||
|
||||
t.transfer = transfer
|
||||
|
||||
t.targetFoldersCache = make(map[string]*MboxList)
|
||||
t.targetLabelsCache = make(map[string]*MboxList)
|
||||
|
||||
t.updateGroupSelection()
|
||||
}
|
||||
|
||||
@ -196,7 +202,9 @@ func (t *TransferRules) targetLabels(sourceID string) *MboxList {
|
||||
// Setters
|
||||
|
||||
func (t *TransferRules) setIsGroupActive(groupName string, isActive bool) {
|
||||
wantExclusive := (groupName == FolderTypeLabel)
|
||||
log.WithField("group", groupName).WithField("active", isActive).Trace("Setting group as active/inactive")
|
||||
|
||||
wantExclusive := (groupName == FolderTypeFolder)
|
||||
for _, rule := range t.transfer.GetRules() {
|
||||
if rule.SourceMailbox.IsExclusive != wantExclusive {
|
||||
continue
|
||||
@ -265,6 +273,7 @@ func (t *TransferRules) addTargetID(sourceID string, targetID string) {
|
||||
newTargetMailboxes = append(newTargetMailboxes, *targetMailboxToAdd)
|
||||
}
|
||||
t.setRule(rule.SourceMailbox, newTargetMailboxes, rule.FromTime, rule.ToTime, []int{RuleTargetLabelColors})
|
||||
t.updateTargetSelection(sourceID, targetMailboxToAdd.IsExclusive)
|
||||
}
|
||||
|
||||
func (t *TransferRules) removeTargetID(sourceID string, targetID string) {
|
||||
@ -286,10 +295,14 @@ func (t *TransferRules) removeTargetID(sourceID string, targetID string) {
|
||||
}
|
||||
}
|
||||
t.setRule(rule.SourceMailbox, newTargetMailboxes, rule.FromTime, rule.ToTime, []int{RuleTargetLabelColors})
|
||||
t.updateTargetSelection(sourceID, targetMailboxToRemove.IsExclusive)
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
// getRule returns rule for given source ID.
|
||||
// WARN: Always get new rule after change because previous pointer points to
|
||||
// outdated struct with old data.
|
||||
func (t *TransferRules) getRule(sourceID string) *transfer.Rule {
|
||||
mailbox := t.getMailbox(t.transfer.SourceMailboxes, sourceID)
|
||||
if mailbox == nil {
|
||||
@ -331,20 +344,19 @@ func (t *TransferRules) unsetRule(sourceMailbox transfer.Mailbox) {
|
||||
}
|
||||
|
||||
func (t *TransferRules) ruleChanged(sourceMailbox transfer.Mailbox, changedRoles []int) {
|
||||
for row, rule := range t.transfer.GetRules() {
|
||||
allRules := t.transfer.GetRules()
|
||||
for row, rule := range allRules {
|
||||
if rule.SourceMailbox.Hash() != sourceMailbox.Hash() {
|
||||
continue
|
||||
}
|
||||
|
||||
t.targetFolders(sourceMailbox.Hash()).itemsChanged(rule)
|
||||
t.targetLabels(sourceMailbox.Hash()).itemsChanged(rule)
|
||||
|
||||
index := t.Index(row, 0, core.NewQModelIndex())
|
||||
if !index.IsValid() || row >= t.rowCount(index) {
|
||||
if !index.IsValid() || row >= len(allRules) {
|
||||
log.WithField("row", row).Warning("Invalid index")
|
||||
return
|
||||
}
|
||||
|
||||
log.WithField("row", row).Trace("Transfer rule changed")
|
||||
t.DataChanged(index, index, changedRoles)
|
||||
break
|
||||
}
|
||||
@ -375,3 +387,16 @@ func (t *TransferRules) updateGroupSelection() {
|
||||
t.SetIsLabelGroupSelected(areAllLabelsSelected)
|
||||
t.SetIsFolderGroupSelected(areAllFoldersSelected)
|
||||
}
|
||||
|
||||
func (t *TransferRules) updateTargetSelection(sourceID string, updateFolderSelect bool) {
|
||||
rule := t.getRule(sourceID)
|
||||
if rule == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if updateFolderSelect {
|
||||
t.targetFolders(rule.SourceMailbox.Hash()).itemsChanged(rule)
|
||||
} else {
|
||||
t.targetLabels(rule.SourceMailbox.Hash()).itemsChanged(rule)
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,6 @@ package qtie
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
"github.com/therecipe/qt/core"
|
||||
)
|
||||
|
||||
@ -181,7 +180,7 @@ func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
s.ConnectStartExport(f.StartExport)
|
||||
s.ConnectStartImport(f.StartImport)
|
||||
|
||||
s.ConnectCheckPathStatus(qtcommon.CheckPathStatus)
|
||||
s.ConnectCheckPathStatus(CheckPathStatus)
|
||||
|
||||
s.ConnectStartUpdate(f.StartUpdate)
|
||||
|
||||
|
||||
@ -43,15 +43,13 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/useragent"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
//"github.com/ProtonMail/proton-bridge/pkg/keychain"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/updates"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/kardianos/osext"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/therecipe/qt/core"
|
||||
|
||||
@ -22,8 +22,8 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/internal/transfer"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/updates"
|
||||
)
|
||||
|
||||
// PanicHandler is an interface of a type that can be used to gracefully handle panics which occur.
|
||||
@ -104,7 +104,7 @@ func (b *bridgeWrap) GetUser(query string) (User, error) {
|
||||
return b.Bridge.GetUser(query)
|
||||
}
|
||||
|
||||
// ImportExporter is an interface of import/export needed by frontend.
|
||||
// ImportExporter is an interface of import-export needed by frontend.
|
||||
type ImportExporter interface {
|
||||
UserManager
|
||||
|
||||
@ -121,9 +121,9 @@ type importExportWrap struct {
|
||||
*importexport.ImportExport
|
||||
}
|
||||
|
||||
// NewImportExportWrap wraps import/export struct into local importExportWrap
|
||||
// NewImportExportWrap wraps import-export struct into local importExportWrap
|
||||
// to implement local interface.
|
||||
// The problem is that Import/Export returns the importexport package's User
|
||||
// The problem is that Import-Export returns the importexport package's User
|
||||
// type. Every method which returns User therefore has to be overridden to
|
||||
// fulfill the interface.
|
||||
func NewImportExportWrap(ie *importexport.ImportExport) *importExportWrap { //nolint[golint]
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// 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 importexport provides core functionality of Import/Export app.
|
||||
// Package importexport provides core functionality of Import-Export app.
|
||||
package importexport
|
||||
|
||||
import (
|
||||
@ -90,7 +90,7 @@ func (ie *ImportExport) ReportFile(osType, osVersion, accountName, address strin
|
||||
defer c.Logout()
|
||||
|
||||
title := "[Import-Export] report file"
|
||||
description := "An import/export report from the user swam down the river."
|
||||
description := "An Import-Export report from the user swam down the river."
|
||||
|
||||
report := pmapi.ReportReq{
|
||||
OS: osType,
|
||||
@ -120,7 +120,7 @@ func (ie *ImportExport) GetLocalImporter(address, path string) (*transfer.Transf
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), ie.config.GetTransferDir(), source, target)
|
||||
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
|
||||
}
|
||||
|
||||
// GetRemoteImporter returns transferrer from remote IMAP to ProtonMail account.
|
||||
@ -133,7 +133,7 @@ func (ie *ImportExport) GetRemoteImporter(address, username, password, host, por
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), ie.config.GetTransferDir(), source, target)
|
||||
return transfer.New(ie.panicHandler, newImportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
|
||||
}
|
||||
|
||||
// GetEMLExporter returns transferrer from ProtonMail account to local EML structure.
|
||||
@ -143,7 +143,7 @@ func (ie *ImportExport) GetEMLExporter(address, path string) (*transfer.Transfer
|
||||
return nil, err
|
||||
}
|
||||
target := transfer.NewEMLProvider(path)
|
||||
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), ie.config.GetTransferDir(), source, target)
|
||||
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
|
||||
}
|
||||
|
||||
// GetMBOXExporter returns transferrer from ProtonMail account to local MBOX structure.
|
||||
@ -153,7 +153,7 @@ func (ie *ImportExport) GetMBOXExporter(address, path string) (*transfer.Transfe
|
||||
return nil, err
|
||||
}
|
||||
target := transfer.NewMBOXProvider(path)
|
||||
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), ie.config.GetTransferDir(), source, target)
|
||||
return transfer.New(ie.panicHandler, newExportMetricsManager(ie), ie.config.GetLogDir(), ie.config.GetTransferDir(), source, target)
|
||||
}
|
||||
|
||||
func (ie *ImportExport) getPMAPIProvider(address string) (*transfer.PMAPIProvider, error) {
|
||||
|
||||
@ -21,7 +21,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||
)
|
||||
|
||||
// storeFactory implements dummy factory creating no store (not needed by Import/Export).
|
||||
// storeFactory implements dummy factory creating no store (not needed by Import-Export).
|
||||
type storeFactory struct{}
|
||||
|
||||
// New does nothing.
|
||||
|
||||
@ -22,5 +22,6 @@ import "github.com/ProtonMail/proton-bridge/internal/users"
|
||||
type Configer interface {
|
||||
users.Configer
|
||||
|
||||
GetLogDir() string
|
||||
GetTransferDir() string
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ const (
|
||||
Daily = Action("daily")
|
||||
)
|
||||
|
||||
// Metrics related to import/export (transfer) process.
|
||||
// Metrics related to import-export (transfer) process.
|
||||
const (
|
||||
// Import is used to group import metrics.
|
||||
Import = Category("import")
|
||||
|
||||
@ -25,6 +25,25 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
var systemFolderMapping = map[string]string{ //nolint[gochecknoglobals]
|
||||
"bin": "Trash",
|
||||
"junk": "Spam",
|
||||
"all": "All Mail",
|
||||
"sent mail": "Sent",
|
||||
"draft": "Drafts",
|
||||
"important": "Starred",
|
||||
// Add more translations.
|
||||
}
|
||||
|
||||
// LeastUsedColor is intended to return color for creating a new inbox or label
|
||||
func LeastUsedColor(mailboxes []Mailbox) string {
|
||||
usedColors := []string{}
|
||||
for _, m := range mailboxes {
|
||||
usedColors = append(usedColors, m.Color)
|
||||
}
|
||||
return pmapi.LeastUsedColor(usedColors)
|
||||
}
|
||||
|
||||
// Mailbox is universal data holder of mailbox details for every provider.
|
||||
type Mailbox struct {
|
||||
ID string
|
||||
@ -43,28 +62,10 @@ func (m Mailbox) Hash() string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(m.Name)))
|
||||
}
|
||||
|
||||
// LeastUsedColor is intended to return color for creating a new inbox or label
|
||||
func LeastUsedColor(mailboxes []Mailbox) string {
|
||||
usedColors := []string{}
|
||||
for _, m := range mailboxes {
|
||||
usedColors = append(usedColors, m.Color)
|
||||
}
|
||||
return pmapi.LeastUsedColor(usedColors)
|
||||
}
|
||||
|
||||
// findMatchingMailboxes returns all matching mailboxes from `mailboxes`.
|
||||
// Only one exclusive mailbox is returned.
|
||||
// Only one exclusive mailbox is included.
|
||||
func (m Mailbox) findMatchingMailboxes(mailboxes []Mailbox) []Mailbox {
|
||||
nameVariants := []string{}
|
||||
if strings.Contains(m.Name, "/") || strings.Contains(m.Name, "|") {
|
||||
for _, slashPart := range strings.Split(m.Name, "/") {
|
||||
for _, part := range strings.Split(slashPart, "|") {
|
||||
nameVariants = append(nameVariants, strings.ToLower(part))
|
||||
}
|
||||
}
|
||||
}
|
||||
nameVariants = append(nameVariants, strings.ToLower(m.Name))
|
||||
|
||||
nameVariants := m.nameVariants()
|
||||
isExclusiveIncluded := false
|
||||
matches := []Mailbox{}
|
||||
for i := range nameVariants {
|
||||
@ -83,3 +84,27 @@ func (m Mailbox) findMatchingMailboxes(mailboxes []Mailbox) []Mailbox {
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
// nameVariants returns all possible variants of the mailbox name.
|
||||
// The best match (original name) is at the end of the slice.
|
||||
// Variants are all in lower case. Examples:
|
||||
// * Foo/bar -> [foo, bar, foo/bar]
|
||||
// * x/Bin -> [x, trash, bin, x/bin]
|
||||
// * a|b/c -> [a, b, c, a|b/c]
|
||||
func (m Mailbox) nameVariants() (nameVariants []string) {
|
||||
name := strings.ToLower(m.Name)
|
||||
if strings.Contains(name, "/") || strings.Contains(name, "|") {
|
||||
for _, slashPart := range strings.Split(name, "/") {
|
||||
for _, part := range strings.Split(slashPart, "|") {
|
||||
if mappedPart, ok := systemFolderMapping[part]; ok {
|
||||
nameVariants = append(nameVariants, strings.ToLower(mappedPart))
|
||||
}
|
||||
nameVariants = append(nameVariants, part)
|
||||
}
|
||||
}
|
||||
}
|
||||
if mappedName, ok := systemFolderMapping[name]; ok {
|
||||
nameVariants = append(nameVariants, strings.ToLower(mappedName))
|
||||
}
|
||||
return append(nameVariants, name)
|
||||
}
|
||||
|
||||
@ -66,6 +66,7 @@ func TestLeastUsedColor(t *testing.T) {
|
||||
}
|
||||
r.Equal(t, "#7569d1", LeastUsedColor(mailboxes))
|
||||
}
|
||||
|
||||
func TestFindMatchingMailboxes(t *testing.T) {
|
||||
mailboxes := []Mailbox{
|
||||
{Name: "Inbox", IsExclusive: true},
|
||||
@ -75,6 +76,8 @@ func TestFindMatchingMailboxes(t *testing.T) {
|
||||
{Name: "hello/world", IsExclusive: true},
|
||||
{Name: "Hello", IsExclusive: false},
|
||||
{Name: "WORLD", IsExclusive: true},
|
||||
{Name: "Trash", IsExclusive: true},
|
||||
{Name: "Drafts", IsExclusive: true},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
@ -88,6 +91,10 @@ func TestFindMatchingMailboxes(t *testing.T) {
|
||||
{"hello/world", []string{"hello/world", "Hello"}},
|
||||
{"hello|world", []string{"WORLD", "Hello"}},
|
||||
{"nomailbox", []string{}},
|
||||
{"bin", []string{"Trash"}},
|
||||
{"root/bin", []string{"Trash"}},
|
||||
{"draft", []string{"Drafts"}},
|
||||
{"root/draft", []string{"Drafts"}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
|
||||
@ -30,11 +30,12 @@ import (
|
||||
// Import and export update progress about processing messages and progress
|
||||
// informs user interface, vice versa action (such as pause or resume) from
|
||||
// user interface is passed down to import and export.
|
||||
type Progress struct {
|
||||
type Progress struct { //nolint[maligned]
|
||||
log *logrus.Entry
|
||||
lock sync.RWMutex
|
||||
lock sync.Locker
|
||||
|
||||
updateCh chan struct{}
|
||||
messageCounted bool
|
||||
messageCounts map[string]uint
|
||||
messageStatuses map[string]*MessageStatus
|
||||
pauseReason string
|
||||
@ -45,7 +46,8 @@ type Progress struct {
|
||||
|
||||
func newProgress(log *logrus.Entry, fileReport *fileReport) Progress {
|
||||
return Progress{
|
||||
log: log,
|
||||
log: log,
|
||||
lock: &sync.Mutex{},
|
||||
|
||||
updateCh: make(chan struct{}),
|
||||
messageCounts: map[string]uint{},
|
||||
@ -57,11 +59,7 @@ func newProgress(log *logrus.Entry, fileReport *fileReport) Progress {
|
||||
// update is helper to notify listener for updates.
|
||||
func (p *Progress) update() {
|
||||
if p.updateCh == nil {
|
||||
// If the progress was ended by fatal instead finish, we ignore error.
|
||||
if p.fatalError != nil {
|
||||
return
|
||||
}
|
||||
panic("update should not be called after finish was called")
|
||||
return
|
||||
}
|
||||
|
||||
// In case no one listens for an update, do not block the progress.
|
||||
@ -71,17 +69,12 @@ func (p *Progress) update() {
|
||||
}
|
||||
}
|
||||
|
||||
// start should be called before anything starts.
|
||||
func (p *Progress) start() {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
}
|
||||
|
||||
// finish should be called as the last call once everything is done.
|
||||
func (p *Progress) finish() {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
log.Debug("Progress finished")
|
||||
p.cleanUpdateCh()
|
||||
}
|
||||
|
||||
@ -90,6 +83,7 @@ func (p *Progress) fatal(err error) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
log.WithError(err).Error("Progress finished")
|
||||
p.isStopped = true
|
||||
p.fatalError = err
|
||||
p.cleanUpdateCh()
|
||||
@ -97,21 +91,26 @@ func (p *Progress) fatal(err error) {
|
||||
|
||||
func (p *Progress) cleanUpdateCh() {
|
||||
if p.updateCh == nil {
|
||||
// If the progress was ended by fatal instead finish, we ignore error.
|
||||
if p.fatalError != nil {
|
||||
return
|
||||
}
|
||||
panic("update should not be called after finish was called")
|
||||
return
|
||||
}
|
||||
|
||||
close(p.updateCh)
|
||||
p.updateCh = nil
|
||||
}
|
||||
|
||||
func (p *Progress) countsFinal() {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
defer p.update()
|
||||
|
||||
log.Info("Estimating count finished")
|
||||
p.messageCounted = true
|
||||
}
|
||||
|
||||
func (p *Progress) updateCount(mailbox string, count uint) {
|
||||
p.lock.Lock()
|
||||
defer p.update()
|
||||
defer p.lock.Unlock()
|
||||
defer p.update()
|
||||
|
||||
log.WithField("mailbox", mailbox).WithField("count", count).Debug("Mailbox count updated")
|
||||
p.messageCounts[mailbox] = count
|
||||
@ -120,8 +119,8 @@ func (p *Progress) updateCount(mailbox string, count uint) {
|
||||
// addMessage should be called as soon as there is ID of the message.
|
||||
func (p *Progress) addMessage(messageID string, rule *Rule) {
|
||||
p.lock.Lock()
|
||||
defer p.update()
|
||||
defer p.lock.Unlock()
|
||||
defer p.update()
|
||||
|
||||
p.log.WithField("id", messageID).Trace("Message added")
|
||||
p.messageStatuses[messageID] = &MessageStatus{
|
||||
@ -134,10 +133,15 @@ func (p *Progress) addMessage(messageID string, rule *Rule) {
|
||||
// messageExported should be called right before message is exported.
|
||||
func (p *Progress) messageExported(messageID string, body []byte, err error) {
|
||||
p.lock.Lock()
|
||||
defer p.update()
|
||||
defer p.lock.Unlock()
|
||||
defer p.update()
|
||||
|
||||
log := p.log.WithField("id", messageID)
|
||||
if err != nil {
|
||||
log = log.WithError(err)
|
||||
}
|
||||
log.Debug("Message exported")
|
||||
|
||||
p.log.WithField("id", messageID).WithError(err).Debug("Message exported")
|
||||
status := p.messageStatuses[messageID]
|
||||
status.exportErr = err
|
||||
if err == nil {
|
||||
@ -148,7 +152,7 @@ func (p *Progress) messageExported(messageID string, body []byte, err error) {
|
||||
status.bodyHash = fmt.Sprintf("%x", sha256.Sum256(body))
|
||||
|
||||
if header, err := getMessageHeader(body); err != nil {
|
||||
p.log.WithField("id", messageID).WithError(err).Warning("Failed to parse headers for reporting")
|
||||
log.WithError(err).Warning("Failed to parse headers for reporting")
|
||||
} else {
|
||||
status.setDetailsFromHeader(header)
|
||||
}
|
||||
@ -163,10 +167,15 @@ func (p *Progress) messageExported(messageID string, body []byte, err error) {
|
||||
// messageImported should be called right after message is imported.
|
||||
func (p *Progress) messageImported(messageID, importID string, err error) {
|
||||
p.lock.Lock()
|
||||
defer p.update()
|
||||
defer p.lock.Unlock()
|
||||
defer p.update()
|
||||
|
||||
log := p.log.WithField("id", messageID)
|
||||
if err != nil {
|
||||
log = log.WithError(err)
|
||||
}
|
||||
log.Debug("Message imported")
|
||||
|
||||
p.log.WithField("id", messageID).WithError(err).Debug("Message imported")
|
||||
p.messageStatuses[messageID].targetID = importID
|
||||
p.messageStatuses[messageID].importErr = err
|
||||
if err == nil {
|
||||
@ -187,6 +196,8 @@ func (p *Progress) logMessage(messageID string) {
|
||||
|
||||
// callWrap calls the callback and in case of problem it pause the process.
|
||||
// Then it waits for user action to fix it and click on continue or abort.
|
||||
// Every function doing I/O should be wrapped by this function to provide
|
||||
// stopping and pausing functionality.
|
||||
func (p *Progress) callWrap(callback func() error) {
|
||||
for {
|
||||
if p.shouldStop() {
|
||||
@ -222,8 +233,8 @@ func (p *Progress) GetUpdateChannel() chan struct{} {
|
||||
// Pause pauses the progress.
|
||||
func (p *Progress) Pause(reason string) {
|
||||
p.lock.Lock()
|
||||
defer p.update()
|
||||
defer p.lock.Unlock()
|
||||
defer p.update()
|
||||
|
||||
p.log.Info("Progress paused")
|
||||
p.pauseReason = reason
|
||||
@ -232,8 +243,8 @@ func (p *Progress) Pause(reason string) {
|
||||
// Resume resumes the progress.
|
||||
func (p *Progress) Resume() {
|
||||
p.lock.Lock()
|
||||
defer p.update()
|
||||
defer p.lock.Unlock()
|
||||
defer p.update()
|
||||
|
||||
p.log.Info("Progress resumed")
|
||||
p.pauseReason = ""
|
||||
@ -258,8 +269,8 @@ func (p *Progress) PauseReason() string {
|
||||
// Stop stops the process.
|
||||
func (p *Progress) Stop() {
|
||||
p.lock.Lock()
|
||||
defer p.update()
|
||||
defer p.lock.Unlock()
|
||||
defer p.update()
|
||||
|
||||
p.log.Info("Progress stopped")
|
||||
p.isStopped = true
|
||||
@ -304,6 +315,12 @@ func (p *Progress) GetCounts() (failed, imported, exported, added, total uint) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
// Return counts only once total is estimated or the process already
|
||||
// ended (for a case when it ended quickly to report it correctly).
|
||||
if p.updateCh != nil && !p.messageCounted {
|
||||
return
|
||||
}
|
||||
|
||||
// Include lost messages in the process only when transfer is done.
|
||||
includeMissing := p.updateCh == nil
|
||||
|
||||
@ -334,10 +351,10 @@ func (p *Progress) GenerateBugReport() []byte {
|
||||
return bugReport.getData()
|
||||
}
|
||||
|
||||
func (p *Progress) FileReport() (path string) {
|
||||
if r := p.fileReport; r != nil {
|
||||
path = r.path
|
||||
// FileReport returns path to generated defailed file report.
|
||||
func (p *Progress) FileReport() string {
|
||||
if p.fileReport == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return
|
||||
return p.fileReport.path
|
||||
}
|
||||
|
||||
@ -29,8 +29,6 @@ func TestProgressUpdateCount(t *testing.T) {
|
||||
progress := newProgress(log, nil)
|
||||
drainProgressUpdateChannel(&progress)
|
||||
|
||||
progress.start()
|
||||
|
||||
progress.updateCount("inbox", 10)
|
||||
progress.updateCount("archive", 20)
|
||||
progress.updateCount("inbox", 12)
|
||||
@ -48,8 +46,6 @@ func TestProgressAddingMessages(t *testing.T) {
|
||||
progress := newProgress(log, nil)
|
||||
drainProgressUpdateChannel(&progress)
|
||||
|
||||
progress.start()
|
||||
|
||||
// msg1 has no problem.
|
||||
progress.addMessage("msg1", nil)
|
||||
progress.messageExported("msg1", []byte(""), nil)
|
||||
@ -92,18 +88,16 @@ func TestProgressFinish(t *testing.T) {
|
||||
progress := newProgress(log, nil)
|
||||
drainProgressUpdateChannel(&progress)
|
||||
|
||||
progress.start()
|
||||
progress.finish()
|
||||
r.Nil(t, progress.updateCh)
|
||||
|
||||
r.Panics(t, func() { progress.addMessage("msg", nil) })
|
||||
r.NotPanics(t, func() { progress.addMessage("msg", nil) })
|
||||
}
|
||||
|
||||
func TestProgressFatalError(t *testing.T) {
|
||||
progress := newProgress(log, nil)
|
||||
drainProgressUpdateChannel(&progress)
|
||||
|
||||
progress.start()
|
||||
progress.fatal(errors.New("fatal error"))
|
||||
r.Nil(t, progress.updateCh)
|
||||
|
||||
|
||||
@ -36,6 +36,10 @@ func (p *EMLProvider) TransferTo(rules transferRules, progress *Progress, ch cha
|
||||
return
|
||||
}
|
||||
|
||||
if len(filePathsPerFolder) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// This list is not filtered by time but instead going throgh each file
|
||||
// twice or keeping all in memory we will tell rough estimation which
|
||||
// will be updated during processing each file.
|
||||
@ -46,6 +50,7 @@ func (p *EMLProvider) TransferTo(rules transferRules, progress *Progress, ch cha
|
||||
|
||||
progress.updateCount(folderName, uint(len(filePaths)))
|
||||
}
|
||||
progress.countsFinal()
|
||||
|
||||
for folderName, filePaths := range filePathsPerFolder {
|
||||
// No error guaranteed by getFilePathsPerFolder.
|
||||
|
||||
@ -21,7 +21,6 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
@ -73,7 +72,7 @@ func (p *EMLProvider) createFolders(rules transferRules) error {
|
||||
|
||||
func (p *EMLProvider) writeFile(msg Message) error {
|
||||
fileName := filepath.Base(msg.ID)
|
||||
if !strings.HasSuffix(fileName, ".eml") {
|
||||
if filepath.Ext(fileName) != ".eml" {
|
||||
fileName += ".eml"
|
||||
}
|
||||
|
||||
|
||||
@ -58,7 +58,7 @@ func (p *IMAPProvider) ID() string {
|
||||
// Mailboxes returns all available folder names from root of EML files.
|
||||
// In case the same folder name is used more than once (for example root/a/foo
|
||||
// and root/b/foo), it's treated as the same folder.
|
||||
func (p *IMAPProvider) Mailboxes(includEmpty, includeAllMail bool) ([]Mailbox, error) {
|
||||
func (p *IMAPProvider) Mailboxes(includeEmpty, includeAllMail bool) ([]Mailbox, error) {
|
||||
mailboxesInfo, err := p.list()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -73,11 +73,11 @@ func (p *IMAPProvider) Mailboxes(includEmpty, includeAllMail bool) ([]Mailbox, e
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasNoSelect || mailbox.Name == "[Gmail]" {
|
||||
if hasNoSelect {
|
||||
continue
|
||||
}
|
||||
|
||||
if !includEmpty || true {
|
||||
if !includeEmpty || true {
|
||||
mailboxStatus, err := p.selectIn(mailbox.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -72,6 +72,7 @@ func (p *IMAPProvider) loadMessageInfoMap(rules transferRules, progress *Progres
|
||||
res[rule.SourceMailbox.Name] = messagesInfo
|
||||
progress.updateCount(rule.SourceMailbox.Name, uint(len(messagesInfo)))
|
||||
}
|
||||
progress.countsFinal()
|
||||
|
||||
return res
|
||||
}
|
||||
@ -109,7 +110,9 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
|
||||
return
|
||||
}
|
||||
}
|
||||
id := fmt.Sprintf("%s_%d:%d", rule.SourceMailbox.Name, uidValidity, imapMessage.Uid)
|
||||
id := getUniqueMessageID(rule.SourceMailbox.Name, uidValidity, imapMessage.Uid)
|
||||
// We use ID as key to ensure we have every unique message only once.
|
||||
// Some IMAP servers responded twice the same message...
|
||||
messagesInfo[id] = imapMessageInfo{
|
||||
id: id,
|
||||
uid: imapMessage.Uid,
|
||||
@ -173,6 +176,10 @@ func (p *IMAPProvider) exportMessages(rule *Rule, progress *Progress, ch chan<-
|
||||
items := []imap.FetchItem{imap.FetchUid, imap.FetchFlags, section.FetchItem()}
|
||||
|
||||
processMessageCallback := func(imapMessage *imap.Message) {
|
||||
if progress.shouldStop() {
|
||||
return
|
||||
}
|
||||
|
||||
id, ok := uidToID[imapMessage.Uid]
|
||||
|
||||
// Sometimes, server sends not requested messages.
|
||||
@ -217,3 +224,7 @@ func (p *IMAPProvider) exportMessage(rule *Rule, id string, imapMessage *imap.Me
|
||||
Targets: rule.TargetMailboxes,
|
||||
}
|
||||
}
|
||||
|
||||
func getUniqueMessageID(mailboxName string, uidValidity, uid uint32) string {
|
||||
return fmt.Sprintf("%s_%d:%d", mailboxName, uidValidity, uid)
|
||||
}
|
||||
|
||||
@ -40,6 +40,10 @@ func (p *MBOXProvider) TransferTo(rules transferRules, progress *Progress, ch ch
|
||||
return
|
||||
}
|
||||
|
||||
if len(filePathsPerFolder) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for folderName, filePaths := range filePathsPerFolder {
|
||||
// No error guaranteed by getFilePathsPerFolder.
|
||||
rule, _ := rules.getRuleBySourceMailboxName(folderName)
|
||||
@ -50,6 +54,7 @@ func (p *MBOXProvider) TransferTo(rules transferRules, progress *Progress, ch ch
|
||||
p.updateCount(rule, progress, filePath)
|
||||
}
|
||||
}
|
||||
progress.countsFinal()
|
||||
|
||||
for folderName, filePaths := range filePathsPerFolder {
|
||||
// No error guaranteed by getFilePathsPerFolder.
|
||||
|
||||
@ -24,6 +24,7 @@ import (
|
||||
pkgMessage "github.com/ProtonMail/proton-bridge/pkg/message"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const pmapiListPageSize = 150
|
||||
@ -59,10 +60,11 @@ func (p *PMAPIProvider) loadCounts(rules transferRules, progress *Progress) {
|
||||
rule := rule
|
||||
progress.callWrap(func() error {
|
||||
_, total, err := p.listMessages(&pmapi.MessagesFilter{
|
||||
LabelID: rule.SourceMailbox.ID,
|
||||
Begin: rule.FromTime,
|
||||
End: rule.ToTime,
|
||||
Limit: 0,
|
||||
AddressID: p.addressID,
|
||||
LabelID: rule.SourceMailbox.ID,
|
||||
Begin: rule.FromTime,
|
||||
End: rule.ToTime,
|
||||
Limit: 0,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("Problem to load counts")
|
||||
@ -72,10 +74,11 @@ func (p *PMAPIProvider) loadCounts(rules transferRules, progress *Progress) {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
progress.countsFinal()
|
||||
}
|
||||
|
||||
func (p *PMAPIProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Message, skipEncryptedMessages bool) {
|
||||
nextID := ""
|
||||
page := 0
|
||||
for {
|
||||
if progress.shouldStop() {
|
||||
break
|
||||
@ -84,30 +87,33 @@ func (p *PMAPIProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Mes
|
||||
isLastPage := true
|
||||
|
||||
progress.callWrap(func() error {
|
||||
// Would be better to filter by Begin and BeginID to be sure
|
||||
// in case user deletes messages during the process, no message
|
||||
// is skipped (paging is off then), but API does not support
|
||||
// filtering by both mentioned fields at the same time.
|
||||
desc := false
|
||||
pmapiMessages, count, err := p.listMessages(&pmapi.MessagesFilter{
|
||||
pmapiMessages, total, err := p.listMessages(&pmapi.MessagesFilter{
|
||||
AddressID: p.addressID,
|
||||
LabelID: rule.SourceMailbox.ID,
|
||||
Begin: rule.FromTime,
|
||||
End: rule.ToTime,
|
||||
BeginID: nextID,
|
||||
PageSize: pmapiListPageSize,
|
||||
Page: 0,
|
||||
Page: page,
|
||||
Sort: "ID",
|
||||
Desc: &desc,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.WithField("label", rule.SourceMailbox.ID).WithField("next", nextID).WithField("count", count).Debug("Listing messages")
|
||||
log.WithFields(logrus.Fields{
|
||||
"label": rule.SourceMailbox.ID,
|
||||
"page": page,
|
||||
"total": total,
|
||||
"count": len(pmapiMessages),
|
||||
}).Debug("Listing messages")
|
||||
|
||||
isLastPage = len(pmapiMessages) < pmapiListPageSize
|
||||
|
||||
// The first ID is the last one from the last page (= do not export twice the same one).
|
||||
if nextID != "" {
|
||||
pmapiMessages = pmapiMessages[1:]
|
||||
}
|
||||
|
||||
for _, pmapiMessage := range pmapiMessages {
|
||||
if progress.shouldStop() {
|
||||
break
|
||||
@ -122,9 +128,7 @@ func (p *PMAPIProvider) transferTo(rule *Rule, progress *Progress, ch chan<- Mes
|
||||
}
|
||||
}
|
||||
|
||||
if !isLastPage {
|
||||
nextID = pmapiMessages[len(pmapiMessages)-1].ID
|
||||
}
|
||||
page++
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -71,6 +71,11 @@ func (p *PMAPIProvider) TransferFrom(rules transferRules, progress *Progress, ch
|
||||
log.Info("Started transfer from channel to PMAPI")
|
||||
defer log.Info("Finished transfer from channel to PMAPI")
|
||||
|
||||
// Cache has to be cleared before each transfer to not contain
|
||||
// old stuff from previous cancelled run.
|
||||
p.importMsgReqMap = map[string]*pmapi.ImportMsgReq{}
|
||||
p.importMsgReqSize = 0
|
||||
|
||||
for msg := range ch {
|
||||
if progress.shouldStop() {
|
||||
break
|
||||
|
||||
@ -229,8 +229,8 @@ func (r *transferRules) getRule(sourceMailbox Mailbox) *Rule {
|
||||
return r.rules[h]
|
||||
}
|
||||
|
||||
// getRules returns all set rules.
|
||||
func (r *transferRules) getRules() []*Rule {
|
||||
// getSortedRules returns all set rules in order by `byRuleOrder`.
|
||||
func (r *transferRules) getSortedRules() []*Rule {
|
||||
rules := []*Rule{}
|
||||
for _, rule := range r.rules {
|
||||
rules = append(rules, rule)
|
||||
|
||||
@ -239,7 +239,7 @@ func TestOrderRules(t *testing.T) {
|
||||
}
|
||||
|
||||
gotMailboxNames := []string{}
|
||||
for _, rule := range transferRules.getRules() {
|
||||
for _, rule := range transferRules.getSortedRules() {
|
||||
gotMailboxNames = append(gotMailboxNames, rule.SourceMailbox.Name)
|
||||
}
|
||||
|
||||
|
||||
@ -34,10 +34,11 @@ type Transfer struct {
|
||||
panicHandler PanicHandler
|
||||
metrics MetricsManager
|
||||
id string
|
||||
dir string
|
||||
logDir string
|
||||
rules transferRules
|
||||
source SourceProvider
|
||||
target TargetProvider
|
||||
rulesCache []*Rule
|
||||
sourceMboxCache []Mailbox
|
||||
targetMboxCache []Mailbox
|
||||
}
|
||||
@ -47,14 +48,14 @@ type Transfer struct {
|
||||
// source := transfer.NewEMLProvider(...)
|
||||
// target := transfer.NewPMAPIProvider(...)
|
||||
// transfer.New(source, target, ...)
|
||||
func New(panicHandler PanicHandler, metrics MetricsManager, transferDir string, source SourceProvider, target TargetProvider) (*Transfer, error) {
|
||||
func New(panicHandler PanicHandler, metrics MetricsManager, logDir, rulesDir string, source SourceProvider, target TargetProvider) (*Transfer, error) {
|
||||
transferID := fmt.Sprintf("%x", sha256.Sum256([]byte(source.ID()+"-"+target.ID())))
|
||||
rules := loadRules(transferDir, transferID)
|
||||
rules := loadRules(rulesDir, transferID)
|
||||
transfer := &Transfer{
|
||||
panicHandler: panicHandler,
|
||||
metrics: metrics,
|
||||
id: transferID,
|
||||
dir: transferDir,
|
||||
logDir: logDir,
|
||||
rules: rules,
|
||||
source: source,
|
||||
target: target,
|
||||
@ -108,16 +109,19 @@ func (t *Transfer) SetGlobalTimeLimit(fromTime, toTime int64) {
|
||||
|
||||
// SetRule sets sourceMailbox for transfer.
|
||||
func (t *Transfer) SetRule(sourceMailbox Mailbox, targetMailboxes []Mailbox, fromTime, toTime int64) error {
|
||||
t.rulesCache = nil
|
||||
return t.rules.setRule(sourceMailbox, targetMailboxes, fromTime, toTime)
|
||||
}
|
||||
|
||||
// UnsetRule unsets sourceMailbox from transfer.
|
||||
func (t *Transfer) UnsetRule(sourceMailbox Mailbox) {
|
||||
t.rulesCache = nil
|
||||
t.rules.unsetRule(sourceMailbox)
|
||||
}
|
||||
|
||||
// ResetRules unsets all rules.
|
||||
func (t *Transfer) ResetRules() {
|
||||
t.rulesCache = nil
|
||||
t.rules.reset()
|
||||
}
|
||||
|
||||
@ -128,7 +132,10 @@ func (t *Transfer) GetRule(sourceMailbox Mailbox) *Rule {
|
||||
|
||||
// GetRules returns all set transfer rules.
|
||||
func (t *Transfer) GetRules() []*Rule {
|
||||
return t.rules.getRules()
|
||||
if t.rulesCache == nil {
|
||||
t.rulesCache = t.rules.getSortedRules()
|
||||
}
|
||||
return t.rulesCache
|
||||
}
|
||||
|
||||
// SourceMailboxes returns mailboxes available at source side.
|
||||
@ -171,7 +178,7 @@ func (t *Transfer) Start() *Progress {
|
||||
t.metrics.Start()
|
||||
|
||||
log := log.WithField("id", t.id)
|
||||
reportFile := newFileReport(t.dir, t.id)
|
||||
reportFile := newFileReport(t.logDir, t.id)
|
||||
progress := newProgress(log, reportFile)
|
||||
|
||||
ch := make(chan Message)
|
||||
@ -179,7 +186,6 @@ func (t *Transfer) Start() *Progress {
|
||||
go func() {
|
||||
defer t.panicHandler.HandlePanic()
|
||||
|
||||
progress.start()
|
||||
t.source.TransferTo(t.rules, &progress, ch)
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
53
internal/updates/bridge_pubkey.gpg
Normal file
53
internal/updates/bridge_pubkey.gpg
Normal file
@ -0,0 +1,53 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBFo9OeEBEAC+fPrLcUBY+YUc5YiMrYJQ6ogrJWMGC00h9fAv3PsrHkBz0z7c
|
||||
QFDyNdNatokFDtZDX115M0vzDwk5NkcjmO7CWbf6nCZcwYqOSrBoH8wNT9uTS/6p
|
||||
R3AHk1r3C/36QG3iWx6Wg4ycRkXWYToT3/yh5waE5BbLi/9TSBAdfJzTyxt4IpZG
|
||||
3OTMnOwuz6eNRWVHkA48CJydWS6M8z+jIsBwFq4nOIChvLjIF42PuAT1VaiCYSmy
|
||||
4sU1YxxWof5z9HY0XghRpd7aUIgzAIsXUbaEXh/3iCZDUMN5LwkyAn+r5j3SMNzk
|
||||
2htF8V7qWE8ldYNVrpeEwyor0x1wMzpbb/C4Y8wXe8rP01d0ApiHVRETzsQk2esf
|
||||
XuSrBCtpyLc6ET1lluiL2sVUUelAPueUQlOyYXfL2X958i0TgBCi6QRPXxbPjCPs
|
||||
d1UzLPCSUNUO+/7fslZCax26d1r1kbHzJLAN1Jer6rxoEDaEiVSCUTnHgykCq5rO
|
||||
C3PScGEdOaIi4H5c6YFZrLmdz409YmJEWLKIPV/u5DpI+YGmAfAevrjkMBgQBOmZ
|
||||
D8Gp19LnRtmqjVh2rVdr8yc5nAjoNOZwanMwD5vCWPUVELWXubNFBv8hqZMxHZqW
|
||||
GrB8x8hkdgiNmuyqsxzBmOEJHWLlvbFhvHhIedT8paU/spL/qJmWp3EB4QARAQAB
|
||||
tExQcm90b24gVGVjaG5vbG9naWVzIEFHIChQcm90b25NYWlsIEJyaWRnZSBkZXZl
|
||||
bG9wZXJzKSA8YnJpZGdlQHByb3Rvbm1haWwuY2g+iQJUBBMBCAA+AhsDBQsJCAcC
|
||||
BhUICQoLAgQWAgMBAh4BAheAFiEE1R5k0+Y+3D7veGTO4sddaOYjSwcFAlv377wF
|
||||
CQO83tsACgkQ4sddaOYjSwfhng//WNhZqr0StuN4KbYdQG+FY+aLijLhiVI3i4j6
|
||||
wUis+7UWFNMUGePsBUrF7zOrzo4Vp16FSRhhpveIbDMVJg4yGlzwN+jZr9FBvF8z
|
||||
kbOqjajkTF3rOyqSQCpZVgeamRt6c4gGQTOwfwxB4K5mVg4rv65ISIKjLUtCZ27g
|
||||
pD6eJs25LhyZQnI65JHpHDkVar7oQ2nbWv0tn2wrrUKBE9hRM5Jn1xGaHYkrYxPe
|
||||
HNDHrqxJUDbPfJhca54M99bs9Qum3KkT1WWU5/0trA0V8eUZa93zydLNynJJcqbq
|
||||
KUYBvOnpzL/0l3hdffmolpUXWFrlFPlOLVQlK4Kc6oQqS2KWBySQHg9klTto1p9c
|
||||
pNZE3sO5+UfleyXW0dN6DcU/xiwoYKJ/+x4JZYtvqH/kP7gve2oznEsLMw6k2QZo
|
||||
O1GihEpoXpOezs46+ER/YGx4ZF2ne2bmYnzoOOZBbGXwsMZTNaa9QJHbc1bz9jjj
|
||||
IFBc1zmrdi0nsbjlvLugEYIbSb/WP0wKwG66zTatslRIQ2unlUJNnWb0E4VLgz9y
|
||||
q57QpvxS7D312dZV0NnAwhyDI+54XAivXTQb0fAGfcgbtKdKpJb1dcAMb9WOBnpr
|
||||
BK7XLsWbJj5v5nB3AuWer7NhUyJB/ogWQtqRUY1bAcI4cB1zFwYq/PL0sbfAHDxx
|
||||
ZEF6Xhi5Ag0EWj054QEQALdPQOlRT1omHljxnN64jFuDXXSIb6zqaBvUwdYoDpV2
|
||||
dfRmzGklsCVA7WHXBmDWbUe9avgO3OO7ANw6/JzzYjP+jwImpJg7cSqTqW8A1U6T
|
||||
YfGXVUV3a/obIEttl7bI9BsUNgmLsBYIwHov+gl/ajKQdALYHCmq3Bj6o7BBeWPp
|
||||
Vpk9dzjcsLVbmNszNGP1Ik5dKE0jZUi6h+YoVuJE9o/+T+jxoqFRpXNsZqWOEKmC
|
||||
HDz6TTs1iTp+CoZ/5g0eKph6XJ+TuNoqF9491IYEFn9oxzsoIBkewTY/fJWmXf++
|
||||
cnpBODrZLF/GoRFc7MW9Kael9vmQ0J7mjM2bFs308lH0rRrfmdlLAU5iKgPv0akx
|
||||
nnnUqvCcoekFMURDtP3z09KZXuOMnt834utd7WLe+LZD6dxs+rPhyDiW80E8Bdlz
|
||||
1Jo+c2g6toIN+uD7/f5gwaZaXhJB0oO7fWSVVo+HJprWBnmf9frgKq1OcS0BNvA+
|
||||
4Aip2hhFqWJAbUQXCyMaeU2WTWIzy0FQ6SEFFy/RM8O5O1HHsDYjtIic9QJ/PqSD
|
||||
0qN7LMlkjR8AdWvAxm95i5GpxDZODldsOneeummvsn3I1jCoULTik7iJVdRuY1V3
|
||||
vfsYAkefGN/n2ga3MvatCJipwoCGsMgUXGTdokXOqKBgMBuBLCkxj2wlol2R9p8R
|
||||
ABEBAAGJAjwEGAEIACYCGwwWIQTVHmTT5j7cPu94ZM7ix11o5iNLBwUCW/fygQUJ
|
||||
A7zhoAAKCRDix11o5iNLB7eTD/4x8I7I7MQV63Z8hDShJixSi49bfXeykzlrZyrA
|
||||
bqNr7JrIKzgX5F1HTU0JF3m+VGkhlpMIlTF/jLq9f1vzmRuiPvux/jItXYbnHFhh
|
||||
lFekwZkXx4nS5iwjpMDt6C1ERftv+Z5yHK91mZsr6eNcfA6VeIdKBQenltZvDVsq
|
||||
HSVEsDhhsKJ473tauwuPXks7cqq8tsSgVzHzRO+CV6HV1b3Muiy5ZA73RC1oIGYT
|
||||
l5zIk1M0h2FIyCfffTBEhZ/dAMErzwcogTA+EAq+OlypTiw2SXZDRx5sQ8T+018k
|
||||
d3zuJZ4PhzJDpzQ627zhy+1M4HPYOHM/nipOkoGl9D8qrFb/DEcoQ6B4FKVRWugJ
|
||||
7ZdtBpnrzh9eVmH9Z1LyKvhSHMSF6iklvIxlCGXas5j71kRg/Yc/aH/St9tV0ZIP
|
||||
1XhwEAY+ul1LCP2YgunCJEJwiG+MZBEZTU5V0gfjdNa/nqNGPOTbLy5oGPV6yWT3
|
||||
b3mx3wudw+aI8MXXPzMBCAn57S7/xuQ4fODx62NOeme/BOnjASbeE3mZ5/3qBbnu
|
||||
YIgVTYNp5frIG3wK8W1r6NY2vYQ0iBIzOCIxnNDjYqsGlpAytX+SM+YY7J9n1dZa
|
||||
UsUfX5Qs+D9VIr/j3jurObPehn9fahCOC2YXicKgSbmQyBLysbFyLT5AMpn5aes0
|
||||
qdwhrw==
|
||||
=B6/F
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
102
internal/updates/compare_versions.go
Normal file
102
internal/updates/compare_versions.go
Normal file
@ -0,0 +1,102 @@
|
||||
// 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 updates
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var nonVersionChars = regexp.MustCompile(`([^0-9.]+)`) //nolint[gochecknoglobals]
|
||||
|
||||
// sanitizeVersion returns only numbers and periods.
|
||||
func sanitizeVersion(version string) string {
|
||||
return nonVersionChars.ReplaceAllString(version, "")
|
||||
}
|
||||
|
||||
// Result can be false positive, but must not be false negative.
|
||||
// Assuming
|
||||
// * dot separated integers format e.g. "A.B.C.…" where A,B,C,… are integers
|
||||
// * `1.1` == `1.1.0` (i.e. first is not newer)
|
||||
// * `1.1.1` > `1.1` (i.e. first is newer)
|
||||
func isFirstVersionNewer(first, second string) (firstIsNewer bool, err error) {
|
||||
first = sanitizeVersion(first)
|
||||
second = sanitizeVersion(second)
|
||||
|
||||
firstIsNewer, err = false, nil
|
||||
if first == second {
|
||||
return
|
||||
}
|
||||
|
||||
firstIsNewer = true
|
||||
var firstArr, secondArr []int
|
||||
if firstArr, err = versionStrToInts(first); err != nil {
|
||||
return
|
||||
}
|
||||
if secondArr, err = versionStrToInts(second); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
verLength := max(len(firstArr), len(secondArr))
|
||||
firstArr = appendZeros(firstArr, verLength)
|
||||
secondArr = appendZeros(secondArr, verLength)
|
||||
|
||||
for i := 0; i < verLength; i++ {
|
||||
if firstArr[i] == secondArr[i] {
|
||||
continue
|
||||
}
|
||||
return firstArr[i] > secondArr[i], nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func versionStrToInts(version string) (intArr []int, err error) {
|
||||
strArr := strings.Split(version, ".")
|
||||
intArr = make([]int, len(strArr))
|
||||
for index, item := range strArr {
|
||||
if item == "" {
|
||||
intArr[index] = 0
|
||||
continue
|
||||
}
|
||||
intArr[index], err = strconv.Atoi(item)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func appendZeros(ints []int, newsize int) []int {
|
||||
size := len(ints)
|
||||
if size >= newsize {
|
||||
return ints
|
||||
}
|
||||
zeros := make([]int, newsize-size)
|
||||
return append(ints, zeros...)
|
||||
}
|
||||
|
||||
func max(ints ...int) (max int) {
|
||||
max = ints[0]
|
||||
for _, a := range ints {
|
||||
if max < a {
|
||||
max = a
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
78
internal/updates/compare_versions_test.go
Normal file
78
internal/updates/compare_versions_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
// 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 updates
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testDataValues struct {
|
||||
expectErr, expectedNewer bool
|
||||
first, second string
|
||||
}
|
||||
type testDataList []testDataValues
|
||||
|
||||
func (tdl *testDataList) add(err, newer bool, first, second string) { //nolint[unparam]
|
||||
*tdl = append(*tdl, testDataValues{err, newer, first, second})
|
||||
}
|
||||
|
||||
func (tdl *testDataList) addFirstIsNewer(first, second string) {
|
||||
tdl.add(false, true, first, second)
|
||||
tdl.add(false, false, second, first)
|
||||
}
|
||||
|
||||
func TestCompareVersion(t *testing.T) {
|
||||
testData := testDataList{}
|
||||
// same is never newer
|
||||
testData.add(false, false, "1.1.1", "1.1.1")
|
||||
testData.add(false, false, "1.1.0", "1.1")
|
||||
testData.add(false, false, "1.0.0", "1")
|
||||
testData.add(false, false, ".1.1", "0.1.1")
|
||||
testData.add(false, false, "0.1.1", ".1.1")
|
||||
|
||||
testData.addFirstIsNewer("1.1.10", "1.1.1")
|
||||
testData.addFirstIsNewer("1.10.1", "1.1.1")
|
||||
testData.addFirstIsNewer("10.1.1", "1.1.1")
|
||||
|
||||
testData.addFirstIsNewer("1.1.1", "0.1.1")
|
||||
testData.addFirstIsNewer("1.1.1", "1.0.1")
|
||||
testData.addFirstIsNewer("1.1.1", "1.1.0")
|
||||
|
||||
testData.addFirstIsNewer("1.1.1", "1")
|
||||
testData.addFirstIsNewer("1.1.1", "1.1")
|
||||
testData.addFirstIsNewer("1.1.1.1", "1.1.1")
|
||||
|
||||
testData.addFirstIsNewer("1.1.1 beta", "1.1.0")
|
||||
testData.addFirstIsNewer("1z.1z.1z", "1.1.0")
|
||||
testData.addFirstIsNewer("1a.1b.1c", "1.1.0")
|
||||
|
||||
for _, td := range testData {
|
||||
t.Log(td)
|
||||
isNewer, err := isFirstVersionNewer(td.first, td.second)
|
||||
if td.expectErr {
|
||||
require.True(t, err != nil, "expected error but got nil for %#v", td)
|
||||
require.True(t, true == isNewer, "error expected but first is not newer for %#v", td)
|
||||
continue
|
||||
}
|
||||
|
||||
require.True(t, err == nil, "expected no error but have %v for %#v", err, td)
|
||||
require.True(t, isNewer == td.expectedNewer, "expected %v but have %v for %#v", td.expectedNewer, isNewer, err, td)
|
||||
}
|
||||
}
|
||||
131
internal/updates/downloader.go
Normal file
131
internal/updates/downloader.go
Normal file
@ -0,0 +1,131 @@
|
||||
// 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 updates
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/dialer"
|
||||
)
|
||||
|
||||
func mkdirAllClear(path string) error {
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.MkdirAll(path, 0750)
|
||||
}
|
||||
|
||||
func downloadToBytes(path string) (out []byte, err error) {
|
||||
var (
|
||||
client *http.Client
|
||||
response *http.Response
|
||||
)
|
||||
client = dialer.DialTimeoutClient()
|
||||
log.WithField("path", path).Trace("Downloading")
|
||||
|
||||
response, err = client.Get(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
out, err = ioutil.ReadAll(response.Body)
|
||||
_ = response.Body.Close()
|
||||
if response.StatusCode < http.StatusOK || http.StatusIMUsed < response.StatusCode {
|
||||
err = errors.New(path + " " + response.Status)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func downloadWithProgress(status *Progress, sourceURL, targetPath string) (err error) {
|
||||
targetFile, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
log.Warnf("Cannot create update file %s: %v", targetPath, err)
|
||||
return
|
||||
}
|
||||
defer targetFile.Close() //nolint[errcheck]
|
||||
|
||||
var (
|
||||
client *http.Client
|
||||
response *http.Response
|
||||
)
|
||||
client = dialer.DialTimeoutClient()
|
||||
response, err = client.Get(sourceURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer response.Body.Close() //nolint[errcheck]
|
||||
|
||||
contentLength, _ := strconv.ParseUint(response.Header.Get("Content-Length"), 10, 64)
|
||||
|
||||
wc := WriteCounter{
|
||||
Status: status,
|
||||
Target: targetFile,
|
||||
Size: contentLength,
|
||||
}
|
||||
|
||||
err = wc.ReadAll(response.Body)
|
||||
return
|
||||
}
|
||||
|
||||
func downloadWithSignature(status *Progress, sourceURL, targetDir string) (localPath string, err error) {
|
||||
localPath = filepath.Join(targetDir, filepath.Base(sourceURL))
|
||||
|
||||
if err = downloadWithProgress(nil, sourceURL+sigExtension, localPath+sigExtension); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = downloadWithProgress(status, sourceURL, localPath); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type WriteCounter struct {
|
||||
Status *Progress
|
||||
Target io.Writer
|
||||
processed, Size, counter uint64
|
||||
}
|
||||
|
||||
func (s *WriteCounter) ReadAll(source io.Reader) (err error) {
|
||||
s.counter = uint64(0)
|
||||
if s.Target == nil {
|
||||
return errors.New("can not read all, target unset")
|
||||
}
|
||||
if source == nil {
|
||||
return errors.New("can not read all, source unset")
|
||||
}
|
||||
_, err = io.Copy(s.Target, io.TeeReader(source, s))
|
||||
return
|
||||
}
|
||||
|
||||
func (s *WriteCounter) Write(p []byte) (int, error) {
|
||||
if s.Status != nil && s.Size != 0 {
|
||||
s.processed += uint64(len(p))
|
||||
fraction := float32(s.processed) / float32(s.Size)
|
||||
if s.counter%uint64(100) == 0 || fraction == 1. {
|
||||
s.Status.UpdateProcessed(fraction)
|
||||
}
|
||||
}
|
||||
s.counter++
|
||||
return len(p), nil
|
||||
}
|
||||
50
internal/updates/progress.go
Normal file
50
internal/updates/progress.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 updates
|
||||
|
||||
const (
|
||||
InfoCurrentVersion = 1 + iota
|
||||
InfoDownloading
|
||||
InfoVerifying
|
||||
InfoUnpacking
|
||||
InfoUpgrading
|
||||
InfoQuitApp
|
||||
InfoRestartApp
|
||||
)
|
||||
|
||||
type Progress struct {
|
||||
Processed float32 // fraction of finished procedure [0.0-1.0]
|
||||
Description int // description by code (needs to be translated anyway)
|
||||
Err error // occurred error
|
||||
channel chan<- Progress
|
||||
}
|
||||
|
||||
func (s *Progress) Update() {
|
||||
s.channel <- *s
|
||||
}
|
||||
|
||||
func (s *Progress) UpdateDescription(description int) {
|
||||
s.Description = description
|
||||
s.Processed = 0
|
||||
s.Update()
|
||||
}
|
||||
|
||||
func (s *Progress) UpdateProcessed(processed float32) {
|
||||
s.Processed = processed
|
||||
s.Update()
|
||||
}
|
||||
108
internal/updates/signature.go
Normal file
108
internal/updates/signature.go
Normal file
@ -0,0 +1,108 @@
|
||||
// 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 updates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
)
|
||||
|
||||
// gpg --export D51E64D3E63EDC3EEF7864CEE2C75D68E6234B07 | xxd -p | tr -d '\n' | xclip
|
||||
const (
|
||||
keyID = "D51E64D3E63EDC3EEF7864CEE2C75D68E6234B07"
|
||||
pubkeyHex = "99020d045a3d39e1011000be7cfacb714058f9851ce5888cad8250ea882b2563060b4d21f5f02fdcfb2b1e4073d33edc4050f235d35ab689050ed6435f5d79334bf30f093936472398eec259b7fa9c265cc18a8e4ab0681fcc0d4fdb934bfea9477007935af70bfdfa406de25b1e96838c9c4645d6613a13dffca1e70684e416cb8bff5348101d7c9cd3cb1b78229646dce4cc9cec2ecfa78d456547900e3c089c9d592e8cf33fa322c07016ae273880a1bcb8c8178d8fb804f555a8826129b2e2c535631c56a1fe73f476345e0851a5deda508833008b1751b6845e1ff788264350c3792f0932027fabe63dd230dce4da1b45f15eea584f25758355ae9784c32a2bd31d70333a5b6ff0b863cc177bcacfd35774029887551113cec424d9eb1f5ee4ab042b69c8b73a113d6596e88bdac55451e9403ee7944253b26177cbd97f79f22d138010a2e9044f5f16cf8c23ec7755332cf09250d50efbfedfb256426b1dba775af591b1f324b00dd497abeabc681036848954825139c7832902ab9ace0b73d270611d39a222e07e5ce98159acb99dcf8d3d62624458b2883d5feee43a48f981a601f01ebeb8e430181004e9990fc1a9d7d2e746d9aa8d5876ad576bf327399c08e834e6706a73300f9bc258f51510b597b9b34506ff21a993311d9a961ab07cc7c86476088d9aecaab31cc198e1091d62e5bdb161bc784879d4fca5a53fb292ffa89996a77101e10011010001b44c50726f746f6e20546563686e6f6c6f67696573204147202850726f746f6e4d61696c2042726964676520646576656c6f7065727329203c6272696467654070726f746f6e6d61696c2e63683e89025404130108003e021b03050b09080702061508090a0b020416020301021e01021780162104d51e64d3e63edc3eef7864cee2c75d68e6234b0705025bf7efbc050903bcdedb000a0910e2c75d68e6234b07e19e0fff58d859aabd12b6e37829b61d406f8563e68b8a32e18952378b88fac148acfbb51614d31419e3ec054ac5ef33abce8e15a75e85491861a6f7886c3315260e321a5cf037e8d9afd141bc5f3391b3aa8da8e44c5deb3b2a92402a5956079a991b7a7388064133b07f0c41e0ae66560e2bbfae484882a32d4b42676ee0a43e9e26cdb92e1c9942723ae491e91c39156abee84369db5afd2d9f6c2bad428113d851339267d7119a1d892b6313de1cd0c7aeac495036cf7c985c6b9e0cf7d6ecf50ba6dca913d56594e7fd2dac0d15f1e5196bddf3c9d2cdca724972a6ea294601bce9e9ccbff497785d7df9a8969517585ae514f94e2d54252b829cea842a4b62960724901e0f64953b68d69f5ca4d644dec3b9f947e57b25d6d1d37a0dc53fc62c2860a27ffb1e09658b6fa87fe43fb82f7b6a339c4b0b330ea4d906683b51a2844a685e939ecece3af8447f606c78645da77b66e6627ce838e6416c65f0b0c65335a6bd4091db7356f3f638e320505cd739ab762d27b1b8e5bcbba011821b49bfd63f4c0ac06ebacd36adb25448436ba795424d9d66f413854b833f72ab9ed0a6fc52ec3df5d9d655d0d9c0c21c8323ee785c08af5d341bd1f0067dc81bb4a74aa496f575c00c6fd58e067a6b04aed72ec59b263e6fe6707702e59eafb361532241fe881642da91518d5b01c238701d7317062afcf2f4b1b7c01c3c7164417a5e18b9020d045a3d39e1011000b74f40e9514f5a261e58f19cdeb88c5b835d74886facea681bd4c1d6280e957675f466cc6925b02540ed61d70660d66d47bd6af80edce3bb00dc3afc9cf36233fe8f0226a4983b712a93a96f00d54e9361f1975545776bfa1b204b6d97b6c8f41b1436098bb01608c07a2ffa097f6a32907402d81c29aadc18faa3b0417963e956993d7738dcb0b55b98db333463f5224e5d284d236548ba87e62856e244f68ffe4fe8f1a2a151a5736c66a58e10a9821c3cfa4d3b35893a7e0a867fe60d1e2a987a5c9f93b8da2a17de3dd48604167f68c73b2820191ec1363f7c95a65dffbe727a41383ad92c5fc6a1115cecc5bd29a7a5f6f990d09ee68ccd9b16cdf4f251f4ad1adf99d94b014e622a03efd1a9319e79d4aaf09ca1e905314443b4fdf3d3d2995ee38c9edf37e2eb5ded62def8b643e9dc6cfab3e1c83896f3413c05d973d49a3e73683ab6820dfae0fbfdfe60c1a65a5e1241d283bb7d6495568f87269ad606799ff5fae02aad4e712d0136f03ee008a9da1845a962406d44170b231a794d964d6233cb4150e92105172fd133c3b93b51c7b03623b4889cf5027f3ea483d2a37b2cc9648d1f00756bc0c66f798b91a9c4364e0e576c3a779eba69afb27dc8d630a850b4e293b88955d46e635577bdfb1802479f18dfe7da06b732f6ad0898a9c28086b0c8145c64dda245cea8a060301b812c29318f6c25a25d91f69f11001101000189023c041801080026021b0c162104d51e64d3e63edc3eef7864cee2c75d68e6234b0705025bf7f281050903bce1a0000a0910e2c75d68e6234b07b7930ffe31f08ec8ecc415eb767c8434a1262c528b8f5b7d77b293396b672ac06ea36bec9ac82b3817e45d474d4d091779be54692196930895317f8cbabd7f5bf3991ba23efbb1fe322d5d86e71c58619457a4c19917c789d2e62c23a4c0ede82d4445fb6ff99e721caf75999b2be9e35c7c0e9578874a0507a796d66f0d5b2a1d2544b03861b0a278ef7b5abb0b8f5e4b3b72aabcb6c4a05731f344ef8257a1d5d5bdccba2cb9640ef7442d68206613979cc8935334876148c827df7d3044859fdd00c12bcf072881303e100abe3a5ca94e2c36497643471e6c43c4fed35f24777cee259e0f873243a7343adbbce1cbed4ce073d838733f9e2a4e9281a5f43f2aac56ff0c472843a07814a5515ae809ed976d0699ebce1f5e5661fd6752f22af8521cc485ea2925bc8c650865dab398fbd64460fd873f687fd2b7db55d1920fd5787010063eba5d4b08fd9882e9c2244270886f8c6411194d4e55d207e374d6bf9ea3463ce4db2f2e6818f57ac964f76f79b1df0b9dc3e688f0c5d73f33010809f9ed2effc6e4387ce0f1eb634e7a67bf04e9e30126de137999e7fdea05b9ee6088154d8369e5fac81b7c0af16d6be8d636bd84348812333822319cd0e362ab06969032b57f9233e618ec9f67d5d65a52c51f5f942cf83f5522bfe3de3bab39b3de867f5f6a108e0b661789c2a049b990c812f2b1b1722d3e403299f969eb34a9dc21af"
|
||||
)
|
||||
|
||||
var (
|
||||
pubkeyRing = openpgp.EntityList{} //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
func singAndVerify(pathToFile string) (err error) {
|
||||
err = signFile(pathToFile)
|
||||
if err != nil {
|
||||
err = verifyFile(pathToFile)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func signFile(pathToFile string) (err error) {
|
||||
if runtime.GOOS != "linux" { //nolint[goconst]
|
||||
return errors.New("tar not implemented only for linux")
|
||||
}
|
||||
// assuming gpg detach-sign creates file with suffix .sig by default.
|
||||
// Lstat does not follow the link i.e. only link is deleted (not link target).
|
||||
if _, err := os.Lstat(pathToFile + sigExtension); !os.IsNotExist(err) {
|
||||
_ = os.Remove(pathToFile + sigExtension)
|
||||
}
|
||||
cmd := exec.Command("gpg", "--local-user", keyID, "--detach-sign", pathToFile) //nolint[gosec]
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func verifyFile(pathToFile string) error {
|
||||
fileReader, err := os.Open(pathToFile) //nolint[gosec]
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileReader.Close() //nolint[errcheck]
|
||||
|
||||
signatureReader, err := os.Open(pathToFile + sigExtension) //nolint[gosec]
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer signatureReader.Close() //nolint[errcheck]
|
||||
|
||||
return verifyBytes(fileReader, signatureReader)
|
||||
}
|
||||
|
||||
func verifyBytes(fileReader, signatureReader io.Reader) (err error) {
|
||||
if _, err = getPubKey(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = openpgp.CheckDetachedSignature(pubkeyRing, fileReader, signatureReader, nil)
|
||||
/*
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if signer == nil || signer.PrimaryKey.KeyId != keyID {
|
||||
return errors.New("Signer with wrong key ID")
|
||||
}
|
||||
*/
|
||||
return
|
||||
}
|
||||
|
||||
// from opengpg/read_test.go
|
||||
func getPubKey() (el openpgp.EntityList, err error) {
|
||||
if pubkeyRing != nil && len(pubkeyRing) != 0 {
|
||||
return pubkeyRing, nil
|
||||
}
|
||||
data, err := hex.DecodeString(pubkeyHex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pubkeyRing, err = openpgp.ReadKeyRing(bytes.NewBuffer(data))
|
||||
return pubkeyRing, err
|
||||
}
|
||||
239
internal/updates/sync.go
Normal file
239
internal/updates/sync.go
Normal file
@ -0,0 +1,239 @@
|
||||
// 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 updates
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
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) {
|
||||
log.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
|
||||
}
|
||||
log.Debug("path to keep ", relPath)
|
||||
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
|
||||
}
|
||||
log.Debug("check path ", relPath)
|
||||
if !existingRelPaths[relPath] {
|
||||
log.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) {
|
||||
log.Error("remove error ", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreFromBackup(backupDir, localPath string) {
|
||||
log.Error("recovering from ", backupDir, " to ", localPath)
|
||||
_ = copyRecursively(backupDir, localPath)
|
||||
}
|
||||
|
||||
func createBackup(srcFile, dstDir string) (err error) {
|
||||
log.Debug("backup ", srcFile, " in ", dstDir)
|
||||
if err = mkdirAllClear(dstDir); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return copyRecursively(srcFile, dstDir)
|
||||
}
|
||||
|
||||
// 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() {
|
||||
log.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)
|
||||
log.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() {
|
||||
log.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 {
|
||||
log.Debug("It is a symlink")
|
||||
linkPath, err := os.Readlink(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("link to ", linkPath)
|
||||
return os.Symlink(linkPath, dstPath)
|
||||
}
|
||||
|
||||
// Create dir and return.
|
||||
if srcIsDir {
|
||||
log.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) {
|
||||
log.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 {
|
||||
log.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 {
|
||||
log.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/updates/sync_test.go
Normal file
157
internal/updates/sync_test.go
Normal file
@ -0,0 +1,157 @@
|
||||
// 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 updates
|
||||
|
||||
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))
|
||||
log.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
|
||||
log.Info("remove all ", srcDir)
|
||||
err := os.RemoveAll(srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.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
|
||||
log.Info("Sync from ", srcDir, " to ", destDir)
|
||||
err = syncFolders(destDir, srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check
|
||||
log.Info("check ", srcDir, " and ", destDir)
|
||||
err = checkThatFilesAreSame(srcDir, destDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// clear after
|
||||
log.Info("remove all ", srcDir)
|
||||
err = os.RemoveAll(srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.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 = log.WriterLevel(logrus.ErrorLevel)
|
||||
cmd.Stdout = log.WriterLevel(logrus.InfoLevel)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func createTestFolder(dirPath, dirType string) error {
|
||||
log.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
|
||||
}
|
||||
126
internal/updates/tar.go
Normal file
126
internal/updates/tar.go
Normal file
@ -0,0 +1,126 @@
|
||||
// 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 updates
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func createTar(tarPath, sourcePath string) error { //nolint[unused]
|
||||
if runtime.GOOS != "linux" {
|
||||
return errors.New("tar not implemented only for linux")
|
||||
}
|
||||
// Check whether it exists and is a directory.
|
||||
if _, err := os.Lstat(sourcePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(tarPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("tar", "-zvcf", absPath, filepath.Base(sourcePath)) //nolint[gosec]
|
||||
cmd.Dir = filepath.Dir(sourcePath)
|
||||
cmd.Stderr = log.WriterLevel(logrus.ErrorLevel)
|
||||
cmd.Stdout = log.WriterLevel(logrus.InfoLevel)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func untarToDir(tarPath, targetDir string, status *Progress) error { //nolint[funlen]
|
||||
// Check whether it exists and is a directory.
|
||||
if ls, err := os.Lstat(targetDir); err == nil {
|
||||
if !ls.IsDir() {
|
||||
return errors.New("not a dir")
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
tgzReader, err := os.Open(tarPath) //nolint[gosec]
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tgzReader.Close() //nolint[errcheck]
|
||||
|
||||
size := uint64(0)
|
||||
if info, err := tgzReader.Stat(); err == nil {
|
||||
size = uint64(info.Size())
|
||||
}
|
||||
|
||||
wc := &WriteCounter{
|
||||
Status: status,
|
||||
Size: size,
|
||||
}
|
||||
|
||||
tarReader, err := gzip.NewReader(io.TeeReader(tgzReader, wc))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileReader := tar.NewReader(tarReader)
|
||||
for {
|
||||
header, err := fileReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if header == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
targetFile := filepath.Join(targetDir, header.Name)
|
||||
info := header.FileInfo()
|
||||
|
||||
// Create symlink.
|
||||
if header.Typeflag == tar.TypeSymlink {
|
||||
if header.Linkname == "" {
|
||||
return errors.New("missing linkname")
|
||||
}
|
||||
if err := os.Symlink(header.Linkname, targetFile); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle case that it is a directory.
|
||||
if info.IsDir() {
|
||||
if err := os.MkdirAll(targetFile, info.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle case that it is a regular file.
|
||||
if err := copyToFileTruncate(fileReader, targetFile, info.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
1
internal/updates/testdata/current_version_linux.json
vendored
Normal file
1
internal/updates/testdata/current_version_linux.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"Version":"1.1.6","ReleaseDate":"10 Jul 19 11:02 +0200","ReleaseNotes":"• Necessary updates reflecting API changes\n• Report wrongly formated messages\n","ReleaseFixedBugs":"• Fixed verification for contacts signed by older or missing key\n• Outlook always shows attachment icon\n","FixedBugs":["• Fixed verification for contacts signed by older or missing key","• Outlook always shows attachment icon",""],"URL":"https://protonmail.com/download/Bridge-Installer.sh","LandingPage":"https://protonmail.com/bridge/download","UpdateFile":"https://protonmail.com/download/bridge_upgrade_linux.tgz","InstallerFile":"https://protonmail.com/download/Bridge-Installer.sh","DebFile":"https://protonmail.com/download/protonmail-bridge_1.1.6-1_amd64.deb","RpmFile":"https://protonmail.com/download/protonmail-bridge-1.1.6-1.x86_64.rpm","PkgFile":"https://protonmail.com/download/PKGBUILD"}
|
||||
BIN
internal/updates/testdata/current_version_linux.json.sig
vendored
Normal file
BIN
internal/updates/testdata/current_version_linux.json.sig
vendored
Normal file
Binary file not shown.
330
internal/updates/updates.go
Normal file
330
internal/updates/updates.go
Normal file
@ -0,0 +1,330 @@
|
||||
// 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 updates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/importexport"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/constants"
|
||||
"github.com/kardianos/osext"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
sigExtension = ".sig"
|
||||
)
|
||||
|
||||
var (
|
||||
Host = "https://protonmail.com" //nolint[gochecknoglobals]
|
||||
DownloadPath = "download" //nolint[gochecknoglobals]
|
||||
|
||||
// BuildType specifies type of build (e.g. QA or beta).
|
||||
BuildType = "" //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
var (
|
||||
log = logrus.WithField("pkg", "bridgeUtils/updates") //nolint[gochecknoglobals]
|
||||
|
||||
installFileSuffix = map[string]string{ //nolint[gochecknoglobals]
|
||||
"darwin": ".dmg",
|
||||
"windows": ".exe",
|
||||
"linux": ".sh",
|
||||
}
|
||||
|
||||
ErrDownloadFailed = errors.New("error happened during download") //nolint[gochecknoglobals]
|
||||
ErrUpdateVerifyFailed = errors.New("cannot verify signature") //nolint[gochecknoglobals]
|
||||
)
|
||||
|
||||
type Updates struct {
|
||||
version string
|
||||
revision string
|
||||
buildTime string
|
||||
releaseNotes string
|
||||
releaseFixedBugs string
|
||||
updateTempDir string
|
||||
landingPagePath string // Based on Host/; default landing page for download.
|
||||
installerFileBaseName string // File for initial install or manual reinstall. per goos [exe, dmg, sh].
|
||||
versionFileBaseName string // Text file containing information about current file. per goos [_linux,_darwin,_windows].json (have .sig file).
|
||||
updateFileBaseName string // File for automatic update. per goos [_linux,_darwin,_windows].tgz (have .sig file).
|
||||
linuxFileBaseName string // Prefix of linux package names.
|
||||
macAppBundleName string // Name of Mac app file in the bundle for update procedure.
|
||||
cachedNewerVersion *VersionInfo // To have info about latest version even when the internet connection drops.
|
||||
}
|
||||
|
||||
// NewBridge inits Updates struct for bridge.
|
||||
func NewBridge(updateTempDir string) *Updates {
|
||||
return &Updates{
|
||||
version: constants.Version,
|
||||
revision: constants.Revision,
|
||||
buildTime: constants.BuildTime,
|
||||
releaseNotes: bridge.ReleaseNotes,
|
||||
releaseFixedBugs: bridge.ReleaseFixedBugs,
|
||||
updateTempDir: updateTempDir,
|
||||
landingPagePath: "bridge/download",
|
||||
installerFileBaseName: "Bridge-Installer",
|
||||
versionFileBaseName: "current_version",
|
||||
updateFileBaseName: "bridge_upgrade",
|
||||
linuxFileBaseName: "protonmail-bridge",
|
||||
macAppBundleName: "ProtonMail Bridge.app",
|
||||
}
|
||||
}
|
||||
|
||||
// NewImportExport inits Updates struct for import-export.
|
||||
func NewImportExport(updateTempDir string) *Updates {
|
||||
return &Updates{
|
||||
version: constants.Version,
|
||||
revision: constants.Revision,
|
||||
buildTime: constants.BuildTime,
|
||||
releaseNotes: importexport.ReleaseNotes,
|
||||
releaseFixedBugs: importexport.ReleaseFixedBugs,
|
||||
updateTempDir: updateTempDir,
|
||||
landingPagePath: "import-export",
|
||||
installerFileBaseName: "Import-Export-Installer",
|
||||
versionFileBaseName: "current_version_ie",
|
||||
updateFileBaseName: "ie_upgrade",
|
||||
linuxFileBaseName: "protonmail-import-export",
|
||||
macAppBundleName: "ProtonMail Import-Export.app",
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Updates) CreateJSONAndSign(deployDir, goos string) error {
|
||||
versionInfo := u.getLocalVersion(goos)
|
||||
versionInfo.Version = sanitizeVersion(versionInfo.Version)
|
||||
|
||||
versionFileName := filepath.Base(u.versionFileURL(goos))
|
||||
versionFilePath := filepath.Join(deployDir, versionFileName)
|
||||
|
||||
txt, err := json.Marshal(versionInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ioutil.WriteFile(versionFilePath, txt, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := singAndVerify(versionFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateFileName := filepath.Base(versionInfo.UpdateFile)
|
||||
updateFilePath := filepath.Join(deployDir, updateFileName)
|
||||
if err := singAndVerify(updateFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Updates) CheckIsUpToDate() (isUpToDate bool, latestVersion VersionInfo, err error) {
|
||||
localVersion := u.GetLocalVersion()
|
||||
latestVersion, err = u.getLatestVersion()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
localIsOld, err := isFirstVersionNewer(latestVersion.Version, localVersion.Version)
|
||||
return !localIsOld, latestVersion, err
|
||||
}
|
||||
|
||||
func (u *Updates) GetDownloadLink() string {
|
||||
latestVersion, err := u.getLatestVersion()
|
||||
if err != nil || latestVersion.InstallerFile == "" {
|
||||
localVersion := u.GetLocalVersion()
|
||||
return localVersion.GetDownloadLink()
|
||||
}
|
||||
return latestVersion.GetDownloadLink()
|
||||
}
|
||||
|
||||
func (u *Updates) GetLocalVersion() VersionInfo {
|
||||
return u.getLocalVersion(runtime.GOOS)
|
||||
}
|
||||
|
||||
func (u *Updates) getLocalVersion(goos string) VersionInfo {
|
||||
version := u.version
|
||||
if BuildType != "" {
|
||||
version += " " + BuildType
|
||||
}
|
||||
|
||||
versionInfo := VersionInfo{
|
||||
Version: version,
|
||||
Revision: u.revision,
|
||||
ReleaseDate: u.buildTime,
|
||||
ReleaseNotes: u.releaseNotes,
|
||||
ReleaseFixedBugs: u.releaseFixedBugs,
|
||||
FixedBugs: strings.Split(u.releaseFixedBugs, "\n"),
|
||||
URL: u.installerFileURL(goos),
|
||||
|
||||
LandingPage: u.landingPageURL(),
|
||||
UpdateFile: u.updateFileURL(goos),
|
||||
InstallerFile: u.installerFileURL(goos),
|
||||
}
|
||||
|
||||
if goos == "linux" {
|
||||
pkgName := u.linuxFileBaseName
|
||||
pkgRel := "1"
|
||||
pkgBase := strings.Join([]string{Host, DownloadPath, pkgName}, "/")
|
||||
|
||||
versionInfo.DebFile = pkgBase + "_" + u.version + "-" + pkgRel + "_amd64.deb"
|
||||
versionInfo.RpmFile = pkgBase + "-" + u.version + "-" + pkgRel + ".x86_64.rpm"
|
||||
versionInfo.PkgFile = strings.Join([]string{Host, DownloadPath, "PKGBUILD"}, "/")
|
||||
}
|
||||
|
||||
return versionInfo
|
||||
}
|
||||
|
||||
func (u *Updates) getLatestVersion() (latestVersion VersionInfo, err error) {
|
||||
version, err := downloadToBytes(u.versionFileURL(runtime.GOOS))
|
||||
if err != nil {
|
||||
if u.cachedNewerVersion != nil {
|
||||
return *u.cachedNewerVersion, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
signature, err := downloadToBytes(u.signatureFileURL(runtime.GOOS))
|
||||
if err != nil {
|
||||
if u.cachedNewerVersion != nil {
|
||||
return *u.cachedNewerVersion, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = verifyBytes(bytes.NewReader(version), bytes.NewReader(signature)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.NewDecoder(bytes.NewReader(version)).Decode(&latestVersion); err != nil {
|
||||
return
|
||||
}
|
||||
if localIsOld, _ := isFirstVersionNewer(latestVersion.Version, u.version); localIsOld {
|
||||
u.cachedNewerVersion = &latestVersion
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (u *Updates) landingPageURL() string {
|
||||
return strings.Join([]string{Host, u.landingPagePath}, "/")
|
||||
}
|
||||
|
||||
func (u *Updates) signatureFileURL(goos string) string {
|
||||
return u.versionFileURL(goos) + sigExtension
|
||||
}
|
||||
|
||||
func (u *Updates) versionFileURL(goos string) string {
|
||||
return strings.Join([]string{Host, DownloadPath, u.versionFileBaseName + "_" + goos + ".json"}, "/")
|
||||
}
|
||||
|
||||
func (u *Updates) installerFileURL(goos string) string {
|
||||
return strings.Join([]string{Host, DownloadPath, u.installerFileBaseName + installFileSuffix[goos]}, "/")
|
||||
}
|
||||
|
||||
func (u *Updates) updateFileURL(goos string) string {
|
||||
return strings.Join([]string{Host, DownloadPath, u.updateFileBaseName + "_" + goos + ".tgz"}, "/")
|
||||
}
|
||||
|
||||
func (u *Updates) StartUpgrade(currentStatus chan<- Progress) { // nolint[funlen]
|
||||
status := &Progress{channel: currentStatus}
|
||||
defer status.Update()
|
||||
|
||||
// Get latest version.
|
||||
var verInfo VersionInfo
|
||||
status.UpdateDescription(InfoCurrentVersion)
|
||||
if verInfo, status.Err = u.getLatestVersion(); status.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if verInfo.UpdateFile == "" {
|
||||
log.Warn("Empty update URL. Update manually.")
|
||||
status.Err = ErrDownloadFailed
|
||||
return
|
||||
}
|
||||
|
||||
// Download.
|
||||
status.UpdateDescription(InfoDownloading)
|
||||
if status.Err = mkdirAllClear(u.updateTempDir); status.Err != nil {
|
||||
return
|
||||
}
|
||||
var updateTar string
|
||||
updateTar, status.Err = downloadWithSignature(
|
||||
status,
|
||||
verInfo.UpdateFile,
|
||||
u.updateTempDir,
|
||||
)
|
||||
if status.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check signature.
|
||||
status.UpdateDescription(InfoVerifying)
|
||||
status.Err = verifyFile(updateTar)
|
||||
if status.Err != nil {
|
||||
log.Warnf("Cannot verify update file %s: %v", updateTar, status.Err)
|
||||
status.Err = ErrUpdateVerifyFailed
|
||||
return
|
||||
}
|
||||
|
||||
// Untar.
|
||||
status.UpdateDescription(InfoUnpacking)
|
||||
status.Err = untarToDir(updateTar, u.updateTempDir, status)
|
||||
if status.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Run upgrade (OS specific).
|
||||
status.UpdateDescription(InfoUpgrading)
|
||||
switch runtime.GOOS {
|
||||
case "windows": //nolint[goconst]
|
||||
cmd := exec.Command("./" + u.installerFileBaseName) // nolint[gosec]
|
||||
cmd.Dir = u.updateTempDir
|
||||
status.Err = cmd.Start()
|
||||
case "darwin":
|
||||
// current path is better then appDir = filepath.Join("/Applications")
|
||||
var exePath string
|
||||
exePath, status.Err = osext.Executable()
|
||||
if status.Err != nil {
|
||||
return
|
||||
}
|
||||
localPath := filepath.Dir(exePath) // Macos
|
||||
localPath = filepath.Dir(localPath) // Contents
|
||||
localPath = filepath.Dir(localPath) // .app
|
||||
|
||||
updatePath := filepath.Join(u.updateTempDir, u.macAppBundleName)
|
||||
log.Warn("localPath ", localPath)
|
||||
log.Warn("updatePath ", updatePath)
|
||||
status.Err = syncFolders(localPath, updatePath)
|
||||
if status.Err != nil {
|
||||
return
|
||||
}
|
||||
status.UpdateDescription(InfoRestartApp)
|
||||
return
|
||||
default:
|
||||
status.Err = errors.New("upgrade for " + runtime.GOOS + " not implemented")
|
||||
}
|
||||
|
||||
status.UpdateDescription(InfoQuitApp)
|
||||
}
|
||||
25
internal/updates/updates_beta.go
Normal file
25
internal/updates/updates_beta.go
Normal file
@ -0,0 +1,25 @@
|
||||
// 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 build_beta
|
||||
|
||||
package updates
|
||||
|
||||
func init() {
|
||||
DownloadPath = "download/beta"
|
||||
BuildType = "beta"
|
||||
}
|
||||
26
internal/updates/updates_qa.go
Normal file
26
internal/updates/updates_qa.go
Normal file
@ -0,0 +1,26 @@
|
||||
// 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 build_qa
|
||||
|
||||
package updates
|
||||
|
||||
func init() {
|
||||
Host = "https://bridgeteam.protontech.ch"
|
||||
DownloadPath = "download/qa"
|
||||
BuildType = "QA"
|
||||
}
|
||||
184
internal/updates/updates_test.go
Normal file
184
internal/updates/updates_test.go
Normal file
@ -0,0 +1,184 @@
|
||||
// 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 updates
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testServerPort = "8999"
|
||||
|
||||
var testUpdateDir string //nolint[gochecknoglobals]
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
setup()
|
||||
code := m.Run()
|
||||
shutdown()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func setup() {
|
||||
var err error
|
||||
testUpdateDir, err = ioutil.TempDir("", "upgrade")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Host = "http://localhost:" + testServerPort
|
||||
go startServer()
|
||||
}
|
||||
|
||||
func shutdown() {
|
||||
_ = os.RemoveAll(testUpdateDir)
|
||||
}
|
||||
|
||||
func startServer() {
|
||||
http.HandleFunc("/download/current_version_linux.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "./testdata/current_version_linux.json")
|
||||
})
|
||||
http.HandleFunc("/download/current_version_linux.json.sig", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "./testdata/current_version_linux.json.sig")
|
||||
})
|
||||
http.HandleFunc("/download/current_version_darwin.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "./testdata/current_version_linux.json")
|
||||
})
|
||||
http.HandleFunc("/download/current_version_darwin.json.sig", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "./testdata/current_version_linux.json.sig")
|
||||
})
|
||||
panic(http.ListenAndServe(":"+testServerPort, nil))
|
||||
}
|
||||
|
||||
func TestCheckBridgeIsUpToDate(t *testing.T) {
|
||||
updates := newTestUpdates("1.1.6")
|
||||
isUpToDate, _, err := updates.CheckIsUpToDate()
|
||||
require.NoError(t, err)
|
||||
require.True(t, isUpToDate, "Bridge should be up to date")
|
||||
}
|
||||
|
||||
func TestCheckBridgeIsNotUpToDate(t *testing.T) {
|
||||
updates := newTestUpdates("1.1.5")
|
||||
isUpToDate, _, err := updates.CheckIsUpToDate()
|
||||
require.NoError(t, err)
|
||||
require.True(t, !isUpToDate, "Bridge should not be up to date")
|
||||
}
|
||||
|
||||
func TestGetLocalVersion(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test because local version for windows is currently not supported by tests.")
|
||||
}
|
||||
updates := newTestUpdates("1")
|
||||
expectedVersion := VersionInfo{
|
||||
Version: "1",
|
||||
Revision: "rev123",
|
||||
ReleaseDate: "42",
|
||||
ReleaseNotes: "• new feature",
|
||||
ReleaseFixedBugs: "• fixed foo",
|
||||
FixedBugs: []string{"• fixed foo"},
|
||||
URL: Host + "/" + DownloadPath + "/Bridge-Installer.sh",
|
||||
|
||||
LandingPage: Host + "/bridge/download",
|
||||
UpdateFile: Host + "/" + DownloadPath + "/bridge_upgrade_linux.tgz",
|
||||
InstallerFile: Host + "/" + DownloadPath + "/Bridge-Installer.sh",
|
||||
|
||||
DebFile: Host + "/" + DownloadPath + "/protonmail-bridge_1-1_amd64.deb",
|
||||
RpmFile: Host + "/" + DownloadPath + "/protonmail-bridge-1-1.x86_64.rpm",
|
||||
PkgFile: Host + "/" + DownloadPath + "/PKGBUILD",
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
expectedVersion.URL = Host + "/" + DownloadPath + "/Bridge-Installer.dmg"
|
||||
expectedVersion.UpdateFile = Host + "/" + DownloadPath + "/bridge_upgrade_darwin.tgz"
|
||||
expectedVersion.InstallerFile = expectedVersion.URL
|
||||
expectedVersion.DebFile = ""
|
||||
expectedVersion.RpmFile = ""
|
||||
expectedVersion.PkgFile = ""
|
||||
}
|
||||
version := updates.GetLocalVersion()
|
||||
require.Equal(t, expectedVersion, version)
|
||||
}
|
||||
|
||||
func TestGetLatestVersion(t *testing.T) {
|
||||
updates := newTestUpdates("1")
|
||||
expectedVersion := VersionInfo{
|
||||
Version: "1.1.6",
|
||||
Revision: "",
|
||||
ReleaseDate: "10 Jul 19 11:02 +0200",
|
||||
ReleaseNotes: "• Necessary updates reflecting API changes\n• Report wrongly formated messages\n",
|
||||
ReleaseFixedBugs: "• Fixed verification for contacts signed by older or missing key\n• Outlook always shows attachment icon\n",
|
||||
FixedBugs: []string{
|
||||
"• Fixed verification for contacts signed by older or missing key",
|
||||
"• Outlook always shows attachment icon",
|
||||
"",
|
||||
},
|
||||
URL: "https://protonmail.com/download/Bridge-Installer.sh",
|
||||
|
||||
LandingPage: "https://protonmail.com/bridge/download",
|
||||
UpdateFile: "https://protonmail.com/download/bridge_upgrade_linux.tgz",
|
||||
InstallerFile: "https://protonmail.com/download/Bridge-Installer.sh",
|
||||
|
||||
DebFile: "https://protonmail.com/download/protonmail-bridge_1.1.6-1_amd64.deb",
|
||||
RpmFile: "https://protonmail.com/download/protonmail-bridge-1.1.6-1.x86_64.rpm",
|
||||
PkgFile: "https://protonmail.com/download/PKGBUILD",
|
||||
}
|
||||
version, err := updates.getLatestVersion()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedVersion, version)
|
||||
}
|
||||
|
||||
func TestStartUpgrade(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skip("skipping test because only upgrading on windows is currently supported by tests.")
|
||||
}
|
||||
|
||||
updates := newTestUpdates("1")
|
||||
progress := make(chan Progress, 1)
|
||||
done := make(chan error)
|
||||
|
||||
go func() {
|
||||
for current := range progress {
|
||||
log.Infof("progress descr: %d processed %f err %v", current.Description, current.Processed, current.Err)
|
||||
if current.Err != nil {
|
||||
done <- current.Err
|
||||
break
|
||||
}
|
||||
}
|
||||
done <- nil
|
||||
}()
|
||||
|
||||
updates.StartUpgrade(progress)
|
||||
close(progress)
|
||||
require.NoError(t, <-done)
|
||||
}
|
||||
|
||||
func newTestUpdates(version string) *Updates {
|
||||
u := NewBridge(testUpdateDir)
|
||||
u.version = version
|
||||
u.revision = "rev123"
|
||||
u.buildTime = "42"
|
||||
u.releaseNotes = "• new feature"
|
||||
u.releaseFixedBugs = "• fixed foo"
|
||||
return u
|
||||
}
|
||||
49
internal/updates/version_info.go
Normal file
49
internal/updates/version_info.go
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 updates
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type VersionInfo struct {
|
||||
Version string
|
||||
Revision string
|
||||
ReleaseDate string // Timestamp generated automatically
|
||||
ReleaseNotes string // List of features, new line separated with leading dot e.g. `• example\n`
|
||||
ReleaseFixedBugs string // List of fixed bugs, same usage as release notes
|
||||
FixedBugs []string // Deprecated list of fixed bugs keeping for backward compatibility (mandatory for working versions up to 1.1.5)
|
||||
URL string // Open browser and download (obsolete replaced by InstallerFile)
|
||||
|
||||
LandingPage string // landing page for manual download
|
||||
UpdateFile string // automatic update file
|
||||
InstallerFile string `json:",omitempty"` // manual update file
|
||||
DebFile string `json:",omitempty"` // debian package file
|
||||
RpmFile string `json:",omitempty"` // red hat package file
|
||||
PkgFile string `json:",omitempty"` // arch PKGBUILD file
|
||||
}
|
||||
|
||||
func (info *VersionInfo) GetDownloadLink() string {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return strings.Join([]string{info.DebFile, info.RpmFile, info.PkgFile}, "\n")
|
||||
default:
|
||||
return info.InstallerFile
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,7 @@ const (
|
||||
sep = "\x00"
|
||||
|
||||
itemLengthBridge = 9
|
||||
itemLengthImportExport = 6 // Old format for Import/Export.
|
||||
itemLengthImportExport = 6 // Old format for Import-Export.
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@ -299,12 +299,11 @@ func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassphra
|
||||
return errors.Wrap(err, "failed to update API user")
|
||||
}
|
||||
|
||||
emails := []string{}
|
||||
for _, address := range client.Addresses() {
|
||||
if u.useOnlyActiveAddresses && address.Receive != pmapi.CanReceive {
|
||||
continue
|
||||
}
|
||||
emails = append(emails, address.Email)
|
||||
var emails []string //nolint[prealloc]
|
||||
if u.useOnlyActiveAddresses {
|
||||
emails = client.Addresses().ActiveEmails()
|
||||
} else {
|
||||
emails = client.Addresses().AllEmails()
|
||||
}
|
||||
|
||||
if _, err = u.credStorer.Add(apiUser.ID, apiUser.Name, auth.GenToken(), hashedPassphrase, emails); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user