From e91cdca6f32793f889a20767b64fee77015ec782 Mon Sep 17 00:00:00 2001 From: Romain Le Jeune Date: Wed, 28 Jun 2023 10:34:01 +0000 Subject: [PATCH] feat(GODT-2716): Make Configuration Statistics persistent. --- internal/configstatus/config_status.go | 146 +++++++++++++++++++ internal/configstatus/types_config_status.go | 54 +++++++ internal/telemetry/configuration_abort.go | 2 + internal/telemetry/configuration_progress.go | 2 + internal/telemetry/configuration_recovery.go | 2 + internal/telemetry/configuration_success.go | 2 + internal/user/config_status.go | 127 ---------------- internal/user/user.go | 5 +- 8 files changed, 211 insertions(+), 129 deletions(-) create mode 100644 internal/configstatus/config_status.go create mode 100644 internal/configstatus/types_config_status.go delete mode 100644 internal/user/config_status.go diff --git a/internal/configstatus/config_status.go b/internal/configstatus/config_status.go new file mode 100644 index 00000000..d73ea198 --- /dev/null +++ b/internal/configstatus/config_status.go @@ -0,0 +1,146 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail 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. +// +// Proton Mail 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 Proton Mail Bridge. If not, see . + +package configstatus + +import ( + "encoding/json" + "fmt" + "os" + "time" + + "github.com/ProtonMail/proton-bridge/v3/internal/safe" + "github.com/sirupsen/logrus" +) + +const version = "1.0.0" + +func LoadConfigurationStatus(filepath string) (*ConfigurationStatus, error) { + status := ConfigurationStatus{ + FilePath: filepath, + DataLock: safe.NewRWMutex(), + Data: &ConfigurationStatusData{}, + } + + if _, err := os.Stat(filepath); err == nil { + if err := status.Load(); err == nil { + return &status, nil + } + logrus.WithError(err).Warn("Cannot load configuration status file. Reset it.") + } + + status.Data.init() + if err := status.Save(); err != nil { + return &status, err + } + return &status, nil +} + +func (status *ConfigurationStatus) Load() error { + bytes, err := os.ReadFile(status.FilePath) + if err != nil { + return err + } + + var metadata MetadataOnly + if err := json.Unmarshal(bytes, &metadata); err != nil { + return err + } + + if metadata.Metadata.Version != version { + return fmt.Errorf("unsupported configstatus file version %s", metadata.Metadata.Version) + } + + return json.Unmarshal(bytes, status.Data) +} + +func (status *ConfigurationStatus) Save() error { + temp := status.FilePath + "_temp" + f, err := os.Create(temp) //nolint:gosec + if err != nil { + return err + } + + err = json.NewEncoder(f).Encode(status.Data) + if err := f.Close(); err != nil { + logrus.WithError(err).Error("Error while closing configstatus file.") + } + if err != nil { + return err + } + + return os.Rename(temp, status.FilePath) +} + +func (status *ConfigurationStatus) Success() error { + status.DataLock.Lock() + defer status.DataLock.Unlock() + + status.Data.init() + status.Data.DataV1.PendingSince = time.Time{} + return status.Save() +} + +func (status *ConfigurationStatus) Failure(err string) error { + status.DataLock.Lock() + defer status.DataLock.Unlock() + + status.Data.init() + status.Data.DataV1.FailureDetails = err + return status.Save() +} + +func (status *ConfigurationStatus) Progress() error { + status.DataLock.Lock() + defer status.DataLock.Unlock() + + status.Data.DataV1.LastProgress = time.Now() + return status.Save() +} + +func (status *ConfigurationStatus) RecordLinkClicked(link uint) error { + status.DataLock.Lock() + defer status.DataLock.Unlock() + + if !status.Data.hasLinkClicked(link) { + status.Data.setClickedLink(link) + return status.Save() + } + return nil +} + +func (data *ConfigurationStatusData) init() { + data.Metadata = Metadata{ + Version: version, + } + data.DataV1.PendingSince = time.Now() + data.DataV1.LastProgress = time.Time{} + data.DataV1.Autoconf = "" + data.DataV1.ClickedLink = 0 + data.DataV1.ReportSent = false + data.DataV1.ReportClick = false + data.DataV1.FailureDetails = "" +} + +func (data *ConfigurationStatusData) setClickedLink(pos uint) { + data.DataV1.ClickedLink |= 1 << pos +} + +func (data *ConfigurationStatusData) hasLinkClicked(pos uint) bool { + val := data.DataV1.ClickedLink & (1 << pos) + return val > 0 +} diff --git a/internal/configstatus/types_config_status.go b/internal/configstatus/types_config_status.go new file mode 100644 index 00000000..032c63ca --- /dev/null +++ b/internal/configstatus/types_config_status.go @@ -0,0 +1,54 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail 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. +// +// Proton Mail 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 Proton Mail Bridge. If not, see . + +package configstatus + +import ( + "time" + + "github.com/ProtonMail/proton-bridge/v3/internal/safe" +) + +type Metadata struct { + Version string `json:"version"` +} + +type MetadataOnly struct { + Metadata Metadata `json:"metadata"` +} + +type DataV1 struct { + PendingSince time.Time `json:"auto_update"` + LastProgress time.Time `json:"last_progress"` + Autoconf string `json:"auto_conf"` + ClickedLink uint64 `json:"clicked_link"` + ReportSent bool `json:"report_sent"` + ReportClick bool `json:"report_click"` + FailureDetails string `json:"failure_details"` +} + +type ConfigurationStatusData struct { + Metadata Metadata `json:"metadata"` + DataV1 DataV1 `json:"dataV1"` +} + +type ConfigurationStatus struct { + FilePath string + DataLock safe.RWMutex + + Data *ConfigurationStatusData +} diff --git a/internal/telemetry/configuration_abort.go b/internal/telemetry/configuration_abort.go index 4e6566cf..a79a7d37 100644 --- a/internal/telemetry/configuration_abort.go +++ b/internal/telemetry/configuration_abort.go @@ -16,3 +16,5 @@ // along with Proton Mail Bridge. If not, see . package telemetry + +// GODT-2711 diff --git a/internal/telemetry/configuration_progress.go b/internal/telemetry/configuration_progress.go index 4e6566cf..42963f5a 100644 --- a/internal/telemetry/configuration_progress.go +++ b/internal/telemetry/configuration_progress.go @@ -16,3 +16,5 @@ // along with Proton Mail Bridge. If not, see . package telemetry + +// GODT-2713 diff --git a/internal/telemetry/configuration_recovery.go b/internal/telemetry/configuration_recovery.go index 4e6566cf..7c1a5055 100644 --- a/internal/telemetry/configuration_recovery.go +++ b/internal/telemetry/configuration_recovery.go @@ -16,3 +16,5 @@ // along with Proton Mail Bridge. If not, see . package telemetry + +// GODT-2714 diff --git a/internal/telemetry/configuration_success.go b/internal/telemetry/configuration_success.go index 4e6566cf..134e2909 100644 --- a/internal/telemetry/configuration_success.go +++ b/internal/telemetry/configuration_success.go @@ -16,3 +16,5 @@ // along with Proton Mail Bridge. If not, see . package telemetry + +// GODT-2710 diff --git a/internal/user/config_status.go b/internal/user/config_status.go deleted file mode 100644 index c18d4e34..00000000 --- a/internal/user/config_status.go +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) 2023 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail 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. -// -// Proton Mail 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 Proton Mail Bridge. If not, see . - -package user - -import ( - "encoding/json" - "os" - "time" -) - -type ConfigurationStatusData struct { - PendingSince time.Time `json:"auto_update"` - LastProgress time.Time `json:"last_progress"` - Autoconf string `json:"auto_conf"` - ClickedLink uint64 `json:"clicked_link"` - ReportSent bool `json:"report_sent"` - ReportClick bool `json:"report_click"` - FailureDetails string `json:"failure_details"` -} - -type ConfigurationStatus struct { - FilePath string - Data ConfigurationStatusData -} - -func LoadConfigurationStatus(filepath string) (*ConfigurationStatus, error) { - status := ConfigurationStatus{ - FilePath: filepath, - Data: ConfigurationStatusData{}, - } - if _, err := os.Stat(filepath); err == nil { - if err := status.Data.load(filepath); err == nil { - return &status, nil - } - } else { - status.Data.init() - if err := status.save(); err == nil { - return &status, nil - } - } - return &status, nil -} - -func (status *ConfigurationStatus) Success() error { - status.Data.init() - status.Data.PendingSince = time.Time{} - return status.save() -} - -func (status *ConfigurationStatus) Failure(err string) error { - status.Data.init() - status.Data.FailureDetails = err - return status.save() -} - -func (status *ConfigurationStatus) Progress() error { - status.Data.LastProgress = time.Now() - return status.save() -} - -func (status *ConfigurationStatus) RecordLinkClicked(link uint) error { - if !status.Data.hasLinkClicked(link) { - status.Data.setClickedLink(link) - return status.save() - } - return nil -} - -func (status *ConfigurationStatus) save() error { - return status.Data.save(status.FilePath) -} - -func (data *ConfigurationStatusData) init() { - data.PendingSince = time.Now() - data.LastProgress = time.Time{} - data.Autoconf = "" - data.ClickedLink = 0 - data.ReportSent = false - data.ReportClick = false - data.FailureDetails = "" -} - -func (data *ConfigurationStatusData) load(filepath string) error { - f, err := os.Open(filepath) // nolint: gosec - if err != nil { - return err - } - - defer func() { _ = f.Close() }() - - return json.NewDecoder(f).Decode(data) -} - -func (data *ConfigurationStatusData) save(filepath string) error { - f, err := os.Create(filepath) // nolint: gosec - if err != nil { - return err - } - - defer func() { _ = f.Close() }() - - return json.NewEncoder(f).Encode(data) -} - -func (data *ConfigurationStatusData) setClickedLink(pos uint) { - data.ClickedLink |= 1 << pos -} - -func (data *ConfigurationStatusData) hasLinkClicked(pos uint) bool { - val := data.ClickedLink & (1 << pos) - return val > 0 -} diff --git a/internal/user/user.go b/internal/user/user.go index 69b14d58..d9e898b2 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -36,6 +36,7 @@ import ( "github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/proton-bridge/v3/internal" + "github.com/ProtonMail/proton-bridge/v3/internal/configstatus" "github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/safe" @@ -94,7 +95,7 @@ type User struct { panicHandler async.PanicHandler - configStatus *ConfigurationStatus + configStatus *configstatus.ConfigurationStatus } // New returns a new user. @@ -130,7 +131,7 @@ func New( }).Info("Creating user object") configStatusFile := filepath.Join(cacheDir, apiUser.ID+".json") - configStatus, err := LoadConfigurationStatus(configStatusFile) + configStatus, err := configstatus.LoadConfigurationStatus(configStatusFile) if err != nil { return nil, fmt.Errorf("failed to init configuration status file: %w", err) }