package storage import ( "encoding/json" "fmt" "io" "os" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/models" "golang.org/x/text/cases" "golang.org/x/text/language" ) // readFromFile reads the servers from server.json. // 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) { file, err := os.Open(filepath) if os.IsNotExist(err) { return servers, nil } else if err != nil { return servers, err } b, err := io.ReadAll(file) if err != nil { return servers, err } if err := file.Close(); err != nil { return servers, err } return s.extractServersFromBytes(b, hardcodedVersions) } func (s *Storage) extractServersFromBytes(b []byte, hardcodedVersions map[string]uint16) ( servers models.AllServers, err error) { rawMessages := make(map[string]json.RawMessage) if err := json.Unmarshal(b, &rawMessages); err != nil { return servers, fmt.Errorf("decoding servers: %w", err) } // Note schema version is at map key "version" as number allProviders := providers.All() servers.ProviderToServers = make(map[string]models.Servers, len(allProviders)) titleCaser := cases.Title(language.English) for _, provider := range allProviders { hardcodedVersion, ok := hardcodedVersions[provider] if !ok { panic(fmt.Sprintf("provider %s not found in hardcoded servers map; "+ "did you add the provider key in the embedded servers.json?", provider)) } rawMessage, ok := rawMessages[provider] if !ok { // If the provider is not found in the data bytes, just don't set it in // the providers map. That way the hardcoded servers will override them. // This is user provided and could come from different sources in the // future (e.g. a file or API request). continue } mergedServers, versionsMatch, err := s.readServers(provider, hardcodedVersion, rawMessage, titleCaser) if err != nil { return models.AllServers{}, err } else if !versionsMatch { // mergedServers is the empty struct in this case, so don't set the key // in the providerToServers map. continue } servers.ProviderToServers[provider] = mergedServers } return servers, nil } func (s *Storage) readServers(provider string, hardcodedVersion uint16, rawMessage json.RawMessage, titleCaser cases.Caser) (servers models.Servers, versionsMatch bool, err error) { provider = titleCaser.String(provider) var versionObject struct { Version uint16 `json:"version"` } err = json.Unmarshal(rawMessage, &versionObject) 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 } err = json.Unmarshal(rawMessage, &servers) if err != nil { return servers, false, fmt.Errorf("decoding servers for provider %s: %w", provider, err) } return servers, versionsMatch, nil }