mirror of
https://github.com/qdm12/gluetun.git
synced 2026-08-10 06:12:54 +02:00
feat(storage): storage file structure changes (#3301)
- migrate persisted server data storage from `/gluetun/servers.json` to `/gluetun/servers/` - add `STORAGE_SERVERS_ENABLED=on` to enable or disable on-disk server data storage - add `STORAGE_SERVERS_DIRECTORY_PATH=/gluetun/servers` to configure where per-provider server files are stored - keep backward compatibility with legacy `STORAGE_FILEPATH=/gluetun/servers.json` - automatically read and migrate legacy `/gluetun/servers.json` into the new `/gluetun/servers/` layout when needed - try to remove the legacy servers file after a successful migration to the new storage directory - switch persisted server data from one large JSON file to a manifest plus per-provider JSON files - add `UPDATER_PREFER_DIRECT_DOWNLOAD` to allow preferring direct download of provider server data - keep deprecated updater flags `-enduser` and `-maintainer` as no-op warnings for backward compatibility - preserve compatibility checks so persisted server data is discarded when its schema version no longer matches the built-in data - allow preferred persisted provider data to override built-in data when versions match - servers data now lives at https://github.com/qdm12/gluetun-servers/tree/main/pkg/servers
This commit is contained in:
+60
-27
@@ -2,6 +2,7 @@ package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -9,44 +10,76 @@ import (
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
// FlushToFile flushes the merged servers data to the file
|
||||
// specified by path, as indented JSON.
|
||||
func (s *Storage) FlushToFile(path string) error {
|
||||
s.mergedMutex.RLock()
|
||||
defer s.mergedMutex.RUnlock()
|
||||
// flushToFile flushes the merged servers data to files
|
||||
// using the manifest file path given. It is not thread-safe.
|
||||
func (s *Storage) flushToFile(manifestPath string) error {
|
||||
const (
|
||||
filePermission = 0o644
|
||||
dirPermission = 0o755
|
||||
)
|
||||
|
||||
return s.flushToFile(path)
|
||||
}
|
||||
|
||||
// flushToFile flushes the merged servers data to the file
|
||||
// specified by path, as indented JSON. It is not thread-safe.
|
||||
func (s *Storage) flushToFile(path string) error {
|
||||
if path == "" {
|
||||
return nil // no file to write to
|
||||
}
|
||||
const permission = 0o644
|
||||
dirPath := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dirPath, permission); err != nil {
|
||||
return err
|
||||
serversDirectoryPath := filepath.Dir(manifestPath)
|
||||
if err := os.MkdirAll(serversDirectoryPath, dirPermission); err != nil {
|
||||
return fmt.Errorf("creating directory: %w", err)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, permission)
|
||||
for provider, providerServers := range s.mergedServers.ProviderToServers {
|
||||
providerFilepath := providerServers.Filepath
|
||||
if providerFilepath == "" {
|
||||
providerFilepath = filepath.Join(serversDirectoryPath, provider+".json")
|
||||
}
|
||||
|
||||
providerDirectoryPath := filepath.Dir(providerFilepath)
|
||||
if err := os.MkdirAll(providerDirectoryPath, dirPermission); err != nil {
|
||||
return fmt.Errorf("creating directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
metadata := map[string]any{"version": s.mergedServers.Version}
|
||||
|
||||
for provider, providerServers := range s.mergedServers.ProviderToServers {
|
||||
sort.Sort(models.SortableServers(providerServers.Servers))
|
||||
|
||||
providerFilepath := providerServers.Filepath
|
||||
if providerFilepath == "" {
|
||||
providerFilepath = filepath.Join(serversDirectoryPath, provider+".json")
|
||||
}
|
||||
|
||||
providerFile, err := os.OpenFile(providerFilepath,
|
||||
os.O_CREATE|os.O_WRONLY|os.O_TRUNC, filePermission)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening servers data file for %s: %w", provider, err)
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(providerFile)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(providerServers)
|
||||
if err != nil {
|
||||
_ = providerFile.Close()
|
||||
return fmt.Errorf("encoding servers data for %s: %w", provider, err)
|
||||
}
|
||||
|
||||
err = providerFile.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing servers data file for %s: %w", provider, err)
|
||||
}
|
||||
|
||||
metadata[provider] = map[string]string{"filepath": providerFilepath}
|
||||
}
|
||||
|
||||
serversFile, err := os.OpenFile(manifestPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, filePermission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder := json.NewEncoder(serversFile)
|
||||
encoder.SetIndent("", " ")
|
||||
|
||||
for _, obj := range s.mergedServers.ProviderToServers {
|
||||
sort.Sort(models.SortableServers(obj.Servers))
|
||||
}
|
||||
|
||||
err = encoder.Encode(&s.mergedServers)
|
||||
err = encoder.Encode(metadata)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
_ = serversFile.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return file.Close()
|
||||
return serversFile.Close()
|
||||
}
|
||||
|
||||
@@ -3,19 +3,50 @@ package storage
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
serversmodule "github.com/qdm12/gluetun-servers/pkg/servers"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
//go:embed servers.json
|
||||
var allServersEmbedFS embed.FS
|
||||
|
||||
func parseHardcodedServers() (allServers models.AllServers, err error) {
|
||||
func parseHardcodedServers() (allServers models.AllServers) {
|
||||
f, err := allServersEmbedFS.Open("servers.json")
|
||||
if err != nil {
|
||||
return allServers, err
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close() // no-op
|
||||
decoder := json.NewDecoder(f)
|
||||
err = decoder.Decode(&allServers)
|
||||
return allServers, err
|
||||
if err != nil {
|
||||
panic("decoding servers.json: " + err.Error())
|
||||
}
|
||||
|
||||
for provider, metadata := range allServers.ProviderToServers {
|
||||
filename := path.Base(metadata.Filepath)
|
||||
providerFile, err := serversmodule.Files.Open(filename)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("reading embedded provider file %s for %s: %s", filename, provider, err))
|
||||
}
|
||||
defer providerFile.Close() // no-op
|
||||
|
||||
var providerServers models.Servers
|
||||
decoder := json.NewDecoder(providerFile)
|
||||
err = decoder.Decode(&providerServers)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("JSON decoding embedded provider file %s for %s: %s",
|
||||
filename, provider, err))
|
||||
} else if providerServers.Filepath != "" {
|
||||
panic(fmt.Sprintf("embedded provider file %s for %s should not have filepath set",
|
||||
filename, provider))
|
||||
}
|
||||
|
||||
providerServers.Filepath = metadata.Filepath // inherit filepath from servers.json
|
||||
allServers.ProviderToServers[provider] = providerServers
|
||||
}
|
||||
|
||||
return allServers
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/gluetun-servers/pkg/servers"
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -11,9 +15,10 @@ import (
|
||||
func Test_parseHardcodedServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
servers, err := parseHardcodedServers()
|
||||
|
||||
require.NoError(t, err)
|
||||
var servers models.AllServers
|
||||
assert.NotPanics(t, func() {
|
||||
servers = parseHardcodedServers()
|
||||
})
|
||||
|
||||
// all providers minus custom
|
||||
allProviders := providers.All()
|
||||
@@ -24,3 +29,35 @@ func Test_parseHardcodedServers(t *testing.T) {
|
||||
assert.NotEmptyf(t, servers, "for provider %s", provider)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseHardcodedServers_filepathsAndEmbeddedProviderFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hardcodedServers := parseHardcodedServers()
|
||||
|
||||
allProviders := providers.All()
|
||||
for _, provider := range allProviders {
|
||||
providerServers, ok := hardcodedServers.ProviderToServers[provider]
|
||||
require.Truef(t, ok, "for provider %s", provider)
|
||||
|
||||
require.NotEmptyf(t, providerServers.Filepath,
|
||||
"embedded servers filepath should be set for provider %s", provider)
|
||||
|
||||
filename := path.Base(providerServers.Filepath)
|
||||
file, err := servers.Files.Open(filename)
|
||||
require.NoErrorf(t, err, "opening embedded provider file for %s", provider)
|
||||
|
||||
var fileServers struct {
|
||||
Version uint16 `json:"version"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Servers []json.RawMessage `json:"servers"`
|
||||
}
|
||||
err = json.NewDecoder(file).Decode(&fileServers)
|
||||
require.NoErrorf(t, err, "decoding embedded provider file for %s", provider)
|
||||
require.NoError(t, file.Close())
|
||||
|
||||
assert.NotZerof(t, fileServers.Version, "for provider %s", provider)
|
||||
assert.NotZerof(t, fileServers.Timestamp, "for provider %s", provider)
|
||||
assert.NotEmptyf(t, fileServers.Servers, "for provider %s", provider)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,20 @@ func (s *Storage) mergeServers(hardcoded, persisted models.AllServers) models.Al
|
||||
func (s *Storage) mergeProviderServers(provider string,
|
||||
hardcoded, persisted models.Servers,
|
||||
) (merged models.Servers) {
|
||||
if persisted.Preferred && persisted.Version != hardcoded.Version {
|
||||
s.logger.Warn(fmt.Sprintf(
|
||||
"persisted preferred %s servers are discarded because they have version %d and hardcoded servers have version %d",
|
||||
provider, persisted.Version, hardcoded.Version))
|
||||
}
|
||||
|
||||
// If persisted data is marked as preferred, use it regardless of timestamp
|
||||
// (as long as versions match)
|
||||
if persisted.Preferred && persisted.Version == hardcoded.Version && len(persisted.Servers) > 0 {
|
||||
s.logger.Info(fmt.Sprintf(
|
||||
"Using %s servers from file (marked as preferred)", provider))
|
||||
return persisted
|
||||
}
|
||||
|
||||
nowTimestamp := time.Now().Unix()
|
||||
if persisted.Timestamp > nowTimestamp {
|
||||
s.logger.Warn(fmt.Sprintf(
|
||||
|
||||
@@ -45,6 +45,23 @@ func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0)
|
||||
}
|
||||
|
||||
// Infof mocks base method.
|
||||
func (m *MockLogger) Infof(arg0 string, arg1 ...interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
m.ctrl.Call(m, "Infof", varargs...)
|
||||
}
|
||||
|
||||
// Infof indicates an expected call of Infof.
|
||||
func (mr *MockLoggerMockRecorder) Infof(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...)
|
||||
}
|
||||
|
||||
// Warn mocks base method.
|
||||
func (m *MockLogger) Warn(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
+79
-22
@@ -12,29 +12,30 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// readFromFile reads the servers from server.json.
|
||||
// readFromFile reads the servers data starting from the given manifest file path.
|
||||
// It only reads servers that have the same version as the hardcoded servers version
|
||||
// to avoid JSON decoding errors.
|
||||
func (s *Storage) readFromFile(filepath string, hardcodedVersions map[string]uint16) (
|
||||
servers models.AllServers, err error,
|
||||
func (s *Storage) readFromFile(manifestPath string, hardcodedVersions map[string]uint16) (
|
||||
servers models.AllServers, found bool, err error,
|
||||
) {
|
||||
file, err := os.Open(filepath)
|
||||
file, err := os.Open(manifestPath)
|
||||
if os.IsNotExist(err) {
|
||||
return servers, nil
|
||||
return servers, false, nil
|
||||
} else if err != nil {
|
||||
return servers, err
|
||||
return servers, false, err
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return servers, err
|
||||
return servers, true, err
|
||||
}
|
||||
|
||||
if err := file.Close(); err != nil {
|
||||
return servers, err
|
||||
return servers, true, err
|
||||
}
|
||||
|
||||
return s.extractServersFromBytes(b, hardcodedVersions)
|
||||
servers, err = s.extractServersFromBytes(b, hardcodedVersions)
|
||||
return servers, true, err
|
||||
}
|
||||
|
||||
func (s *Storage) extractServersFromBytes(b []byte, hardcodedVersions map[string]uint16) (
|
||||
@@ -46,6 +47,12 @@ func (s *Storage) extractServersFromBytes(b []byte, hardcodedVersions map[string
|
||||
}
|
||||
|
||||
// Note schema version is at map key "version" as number
|
||||
if rawVersion, ok := rawMessages["version"]; ok {
|
||||
err := json.Unmarshal(rawVersion, &servers.Version)
|
||||
if err != nil {
|
||||
return servers, fmt.Errorf("decoding servers schema version: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
allProviders := providers.All()
|
||||
servers.ProviderToServers = make(map[string]models.Servers, len(allProviders))
|
||||
@@ -86,25 +93,20 @@ func (s *Storage) readServers(provider string, hardcodedVersion uint16,
|
||||
) {
|
||||
provider = titleCaser.String(provider)
|
||||
|
||||
var versionObject struct {
|
||||
Version uint16 `json:"version"`
|
||||
var metadata struct {
|
||||
Version uint16 `json:"version"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Filepath string `json:"filepath"`
|
||||
}
|
||||
|
||||
err = json.Unmarshal(rawMessage, &versionObject)
|
||||
err = json.Unmarshal(rawMessage, &metadata)
|
||||
if err != nil {
|
||||
return servers, false, fmt.Errorf("decoding servers version for provider %s: %w",
|
||||
provider, err)
|
||||
}
|
||||
|
||||
persistedVersion := versionObject.Version
|
||||
|
||||
versionsMatch = hardcodedVersion == persistedVersion
|
||||
if !versionsMatch {
|
||||
s.logger.Info(fmt.Sprintf(
|
||||
"%s servers from file discarded because they have "+
|
||||
"version %d and hardcoded servers have version %d",
|
||||
provider, persistedVersion, hardcodedVersion))
|
||||
return servers, versionsMatch, nil
|
||||
if metadata.Filepath != "" {
|
||||
return s.readServersFromFilepath(provider, metadata.Filepath, hardcodedVersion)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(rawMessage, &servers)
|
||||
@@ -113,5 +115,60 @@ func (s *Storage) readServers(provider string, hardcodedVersion uint16,
|
||||
provider, err)
|
||||
}
|
||||
|
||||
return servers, versionsMatch, nil
|
||||
const sourcePath = ""
|
||||
if !checkVersions(hardcodedVersion, servers.Version, provider, sourcePath,
|
||||
servers.Preferred, s.logger) {
|
||||
return models.Servers{}, false, nil
|
||||
}
|
||||
|
||||
return servers, true, nil
|
||||
}
|
||||
|
||||
func (s *Storage) readServersFromFilepath(provider, filepath string, hardcodedVersion uint16) (
|
||||
referencedServers models.Servers, versionsMatch bool, err error,
|
||||
) {
|
||||
providerFile, err := os.Open(filepath)
|
||||
if os.IsNotExist(err) {
|
||||
return models.Servers{}, false, nil
|
||||
} else if err != nil {
|
||||
return models.Servers{}, false, fmt.Errorf("opening servers file %s for provider %s: %w",
|
||||
filepath, provider, err)
|
||||
}
|
||||
defer providerFile.Close()
|
||||
|
||||
err = json.NewDecoder(providerFile).Decode(&referencedServers)
|
||||
if err != nil {
|
||||
return models.Servers{}, false, fmt.Errorf("decoding servers file %s for provider %s: %w",
|
||||
filepath, provider, err)
|
||||
}
|
||||
|
||||
if !checkVersions(hardcodedVersion, referencedServers.Version, provider, filepath,
|
||||
referencedServers.Preferred, s.logger) {
|
||||
return models.Servers{}, false, nil
|
||||
}
|
||||
|
||||
referencedServers.Filepath = filepath
|
||||
return referencedServers, true, nil
|
||||
}
|
||||
|
||||
func checkVersions(builtinVersion, version uint16, provider, sourcePath string,
|
||||
preferred bool, logger Logger,
|
||||
) (match bool) {
|
||||
if version == builtinVersion {
|
||||
return true
|
||||
}
|
||||
name := provider
|
||||
log := logger.Info
|
||||
if preferred {
|
||||
name += " preferred"
|
||||
log = logger.Warn
|
||||
}
|
||||
name += " servers"
|
||||
if sourcePath != "" {
|
||||
name += " from file " + sourcePath
|
||||
}
|
||||
log(fmt.Sprintf(
|
||||
"%s discarded because they have version %d and hardcoded servers have version %d",
|
||||
name, version, builtinVersion))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -30,25 +30,30 @@ func Test_extractServersFromBytes(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
b []byte
|
||||
hardcodedVersions map[string]uint16
|
||||
logged []string
|
||||
makeLogger func(ctrl *gomock.Controller) *MockLogger
|
||||
persisted models.AllServers
|
||||
errMessage string
|
||||
}{
|
||||
"bad JSON": {
|
||||
b: []byte("garbage"),
|
||||
makeLogger: func(_ *gomock.Controller) *MockLogger { return nil },
|
||||
errMessage: "decoding servers: invalid character 'g' looking for beginning of value",
|
||||
},
|
||||
"bad provider JSON": {
|
||||
b: []byte(`{"cyberghost": "garbage"}`),
|
||||
hardcodedVersions: populateProviderToVersion(map[string]uint16{}),
|
||||
makeLogger: func(_ *gomock.Controller) *MockLogger { return nil },
|
||||
errMessage: "decoding servers version for provider Cyberghost: " +
|
||||
"json: cannot unmarshal string into Go value of type struct { Version uint16 \"json:\\\"version\\\"\" }",
|
||||
"json: cannot unmarshal string into Go value of type struct { Version uint16 \"json:\\\"version\\\"\"; " +
|
||||
"Timestamp uint64 \"json:\\\"timestamp\\\"\"; " +
|
||||
"Filepath string \"json:\\\"filepath\\\"\" }",
|
||||
},
|
||||
"bad servers array JSON": {
|
||||
b: []byte(`{"cyberghost": {"version": 1, "servers": "garbage"}}`),
|
||||
hardcodedVersions: populateProviderToVersion(map[string]uint16{
|
||||
providers.Cyberghost: 1,
|
||||
}),
|
||||
makeLogger: func(_ *gomock.Controller) *MockLogger { return nil },
|
||||
errMessage: "decoding servers for provider Cyberghost: " +
|
||||
"json: cannot unmarshal string into Go struct field Servers.servers of type []models.Server",
|
||||
},
|
||||
@@ -57,6 +62,7 @@ func Test_extractServersFromBytes(t *testing.T) {
|
||||
hardcodedVersions: populateProviderToVersion(map[string]uint16{
|
||||
providers.Cyberghost: 1,
|
||||
}),
|
||||
makeLogger: func(_ *gomock.Controller) *MockLogger { return nil },
|
||||
persisted: models.AllServers{
|
||||
ProviderToServers: map[string]models.Servers{},
|
||||
},
|
||||
@@ -68,6 +74,7 @@ func Test_extractServersFromBytes(t *testing.T) {
|
||||
hardcodedVersions: populateProviderToVersion(map[string]uint16{
|
||||
providers.Cyberghost: 1,
|
||||
}),
|
||||
makeLogger: func(_ *gomock.Controller) *MockLogger { return nil },
|
||||
persisted: models.AllServers{
|
||||
ProviderToServers: map[string]models.Servers{
|
||||
providers.Cyberghost: {Version: 1},
|
||||
@@ -81,8 +88,28 @@ func Test_extractServersFromBytes(t *testing.T) {
|
||||
hardcodedVersions: populateProviderToVersion(map[string]uint16{
|
||||
providers.Cyberghost: 2,
|
||||
}),
|
||||
logged: []string{
|
||||
"Cyberghost servers from file discarded because they have version 1 and hardcoded servers have version 2",
|
||||
makeLogger: func(ctrl *gomock.Controller) *MockLogger {
|
||||
logger := NewMockLogger(ctrl)
|
||||
logger.EXPECT().Info("Cyberghost servers discarded because " +
|
||||
"they have version 1 and hardcoded servers have version 2")
|
||||
return logger
|
||||
},
|
||||
persisted: models.AllServers{
|
||||
ProviderToServers: map[string]models.Servers{},
|
||||
},
|
||||
},
|
||||
"preferred_different_versions": {
|
||||
b: []byte(`{
|
||||
"cyberghost": {"version": 1, "timestamp": 1, "preferred": true}
|
||||
}`),
|
||||
hardcodedVersions: populateProviderToVersion(map[string]uint16{
|
||||
providers.Cyberghost: 2,
|
||||
}),
|
||||
makeLogger: func(ctrl *gomock.Controller) *MockLogger {
|
||||
logger := NewMockLogger(ctrl)
|
||||
logger.EXPECT().Warn("Cyberghost preferred servers discarded because " +
|
||||
"they have version 1 and hardcoded servers have version 2")
|
||||
return logger
|
||||
},
|
||||
persisted: models.AllServers{
|
||||
ProviderToServers: map[string]models.Servers{},
|
||||
@@ -95,16 +122,7 @@ func Test_extractServersFromBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
logger := NewMockLogger(ctrl)
|
||||
var previousLogCall *gomock.Call
|
||||
for _, logged := range testCase.logged {
|
||||
call := logger.EXPECT().Info(logged)
|
||||
if previousLogCall != nil {
|
||||
call.After(previousLogCall)
|
||||
}
|
||||
previousLogCall = call
|
||||
}
|
||||
|
||||
logger := testCase.makeLogger(ctrl)
|
||||
s := &Storage{
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants/providers"
|
||||
@@ -10,12 +12,12 @@ import (
|
||||
|
||||
// SetServers sets the given servers for the given provider
|
||||
// in the storage in-memory map and saves all the servers
|
||||
// to file.
|
||||
// to files.
|
||||
// Note the servers given are not copied so the caller must
|
||||
// NOT MUTATE them after calling this method.
|
||||
func (s *Storage) SetServers(provider string, servers []models.Server) (err error) {
|
||||
if provider == providers.Custom {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
s.mergedMutex.Lock()
|
||||
@@ -26,10 +28,24 @@ func (s *Storage) SetServers(provider string, servers []models.Server) (err erro
|
||||
serversObject.Servers = servers
|
||||
s.mergedServers.ProviderToServers[provider] = serversObject
|
||||
|
||||
err = s.flushToFile(s.filepath)
|
||||
if !s.disk {
|
||||
return nil
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(s.directoryPath, manifestFilename)
|
||||
err = s.flushToFile(manifestPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("saving servers to file: %w", err)
|
||||
}
|
||||
|
||||
if !s.hasLegacy() {
|
||||
return nil
|
||||
}
|
||||
s.logger.Infof("removing legacy %s which is now migrated to %s", s.legacyFilepath, s.directoryPath)
|
||||
err = os.Remove(s.legacyFilepath)
|
||||
if err != nil {
|
||||
s.logger.Warn("failed removing legacy servers file " + s.legacyFilepath + ": " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+23
-303854
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
@@ -14,31 +16,38 @@ type Storage struct {
|
||||
// SyncServers method.
|
||||
hardcodedServers models.AllServers
|
||||
logger Logger
|
||||
filepath string
|
||||
disk bool
|
||||
directoryPath string
|
||||
legacyFilepath string
|
||||
}
|
||||
|
||||
const manifestFilename = "manifest.json"
|
||||
|
||||
type Logger interface {
|
||||
Info(s string)
|
||||
Infof(format string, args ...any)
|
||||
Warn(s string)
|
||||
}
|
||||
|
||||
// New creates a new storage and reads the servers from the
|
||||
// embedded servers file and the file on disk.
|
||||
// Passing an empty filepath disables the reading and writing of
|
||||
// embedded servers files and the files on disk.
|
||||
// Passing an empty directoryPath disables the reading and writing of
|
||||
// servers.
|
||||
func New(logger Logger, filepath string) (storage *Storage, err error) {
|
||||
// A unit test prevents any error from being returned
|
||||
func New(logger Logger, disk bool, directoryPath, legacyFilepath string) (storage *Storage, err error) {
|
||||
// A unit test prevents [parseHardcodedServers] from ever failing,
|
||||
// and ensures all providers are part of the servers returned.
|
||||
hardcodedServers, _ := parseHardcodedServers()
|
||||
hardcodedServers := parseHardcodedServers()
|
||||
|
||||
storage = &Storage{
|
||||
hardcodedServers: hardcodedServers,
|
||||
mergedServers: hardcodedServers,
|
||||
logger: logger,
|
||||
filepath: filepath,
|
||||
disk: disk,
|
||||
directoryPath: directoryPath,
|
||||
legacyFilepath: legacyFilepath,
|
||||
}
|
||||
|
||||
if filepath != "" {
|
||||
if disk {
|
||||
if err := storage.syncServers(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -46,3 +55,18 @@ func New(logger Logger, filepath string) (storage *Storage, err error) {
|
||||
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// hasLegacy returns true if the legacy file `legacyFilepath` exists AND is
|
||||
// different from the manifest file defined by `directoryPath`/[manifestFilename].
|
||||
// This is used to determine if the legacy file should be read and removed after flushing servers data.
|
||||
func (s *Storage) hasLegacy() bool {
|
||||
if !s.disk {
|
||||
return false
|
||||
}
|
||||
if filepath.Clean(filepath.Join(s.directoryPath, manifestFilename)) ==
|
||||
filepath.Clean(s.legacyFilepath) {
|
||||
return false
|
||||
}
|
||||
stat, err := os.Stat(s.legacyFilepath)
|
||||
return err == nil && !stat.IsDir()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
@@ -14,18 +16,31 @@ func countServers(allServers models.AllServers) (count int) {
|
||||
return count
|
||||
}
|
||||
|
||||
// syncServers merges the hardcoded servers with the ones from the file.
|
||||
// syncServers merges the hardcoded servers with the ones from on disk files.
|
||||
// It assumes s.directoryPath is set.
|
||||
func (s *Storage) syncServers() (err error) {
|
||||
hardcodedVersions := make(map[string]uint16, len(s.hardcodedServers.ProviderToServers))
|
||||
for provider, servers := range s.hardcodedServers.ProviderToServers {
|
||||
hardcodedVersions[provider] = servers.Version
|
||||
}
|
||||
|
||||
serversOnFile, err := s.readFromFile(s.filepath, hardcodedVersions)
|
||||
sourceManifestPath := filepath.Join(s.directoryPath, manifestFilename)
|
||||
destinationManifestPath := sourceManifestPath
|
||||
serversOnFile, found, err := s.readFromFile(sourceManifestPath, hardcodedVersions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading servers from file: %w", err)
|
||||
}
|
||||
|
||||
hasLegacy := s.hasLegacy()
|
||||
if !found && hasLegacy {
|
||||
sourceManifestPath = s.legacyFilepath
|
||||
s.logger.Infof("reading legacy servers file %s and migrating it to directory %s", sourceManifestPath, s.directoryPath)
|
||||
serversOnFile, _, err = s.readFromFile(sourceManifestPath, hardcodedVersions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading servers from file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
hardcodedCount := countServers(s.hardcodedServers)
|
||||
countOnFile := countServers(serversOnFile)
|
||||
|
||||
@@ -34,13 +49,13 @@ func (s *Storage) syncServers() (err error) {
|
||||
|
||||
if countOnFile == 0 {
|
||||
s.logger.Info(fmt.Sprintf(
|
||||
"creating %s with %d hardcoded servers",
|
||||
s.filepath, hardcodedCount))
|
||||
"writing servers data files to %s with %d hardcoded servers",
|
||||
s.directoryPath, hardcodedCount))
|
||||
s.mergedServers = s.hardcodedServers
|
||||
} else {
|
||||
s.logger.Info(fmt.Sprintf(
|
||||
"merging by most recent %d hardcoded servers and %d servers read from %s",
|
||||
hardcodedCount, countOnFile, s.filepath))
|
||||
"merging by most recent %d hardcoded servers and %d servers read from manifest file %s",
|
||||
hardcodedCount, countOnFile, sourceManifestPath))
|
||||
|
||||
s.mergedServers = s.mergeServers(s.hardcodedServers, serversOnFile)
|
||||
}
|
||||
@@ -50,9 +65,18 @@ func (s *Storage) syncServers() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = s.flushToFile(s.filepath)
|
||||
err = s.flushToFile(destinationManifestPath)
|
||||
if err != nil {
|
||||
s.logger.Warn("failed writing servers to file: " + err.Error())
|
||||
s.logger.Warn("failed writing servers to destination manifest: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
migratedFromLegacy := hasLegacy && sourceManifestPath == s.legacyFilepath
|
||||
if migratedFromLegacy {
|
||||
err = os.Remove(sourceManifestPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
s.logger.Warn("failed removing legacy servers file " + sourceManifestPath + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user