mirror of
https://github.com/safing/portbase
synced 2025-09-04 11:40:23 +00:00
Add support for new index format
This commit is contained in:
parent
0e5eb4b6de
commit
f6fc67ad46
5 changed files with 165 additions and 14 deletions
|
@ -1,13 +1,97 @@
|
||||||
package updater
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// Index describes an index file pulled by the updater.
|
// Index describes an index file pulled by the updater.
|
||||||
type Index struct {
|
type Index struct {
|
||||||
// Path is the path to the index file
|
// Path is the path to the index file
|
||||||
// on the update server.
|
// on the update server.
|
||||||
Path string
|
Path string
|
||||||
|
|
||||||
|
// Channel holds the release channel name of the index.
|
||||||
|
// It must match the filename without extension.
|
||||||
|
Channel string
|
||||||
|
|
||||||
// PreRelease signifies that all versions of this index should be marked as
|
// PreRelease signifies that all versions of this index should be marked as
|
||||||
// pre-releases, no matter if the versions actually have a pre-release tag or
|
// pre-releases, no matter if the versions actually have a pre-release tag or
|
||||||
// not.
|
// not.
|
||||||
PreRelease bool
|
PreRelease bool
|
||||||
|
|
||||||
|
// LastRelease holds the time of the last seen release of this index.
|
||||||
|
LastRelease time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexFile represents an index file.
|
||||||
|
type IndexFile struct {
|
||||||
|
Channel string
|
||||||
|
Published time.Time
|
||||||
|
|
||||||
|
Releases map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrIndexFromFuture is returned when a signed index is parsed with a
|
||||||
|
// Published timestamp that lies in the future.
|
||||||
|
ErrIndexFromFuture = errors.New("index is from the future")
|
||||||
|
|
||||||
|
// ErrIndexIsOlder is returned when a signed index is parsed with an older
|
||||||
|
// Published timestamp than the current Published timestamp.
|
||||||
|
ErrIndexIsOlder = errors.New("index is older than the current one")
|
||||||
|
|
||||||
|
// ErrIndexChannelMismatch is returned when a signed index is parsed with a
|
||||||
|
// different channel that the expected one.
|
||||||
|
ErrIndexChannelMismatch = errors.New("index does not match the expected channel")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseIndexFile parses an index file and checks if it is valid.
|
||||||
|
func ParseIndexFile(indexData []byte, channel string, currentPublished time.Time) (*IndexFile, error) {
|
||||||
|
// Load into struct.
|
||||||
|
indexFile := &IndexFile{}
|
||||||
|
err := json.Unmarshal(indexData, indexFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse signed index data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to old format if there are no releases and no channel is defined.
|
||||||
|
// TODO: Remove in v0.10
|
||||||
|
if len(indexFile.Releases) == 0 && indexFile.Channel == "" {
|
||||||
|
return loadOldIndexFormat(indexData, channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the index metadata.
|
||||||
|
switch {
|
||||||
|
case !indexFile.Published.IsZero() && time.Now().Before(indexFile.Published):
|
||||||
|
return indexFile, ErrIndexFromFuture
|
||||||
|
|
||||||
|
case !indexFile.Published.IsZero() &&
|
||||||
|
!currentPublished.IsZero() &&
|
||||||
|
currentPublished.After(indexFile.Published):
|
||||||
|
return indexFile, ErrIndexIsOlder
|
||||||
|
|
||||||
|
case channel != "" &&
|
||||||
|
indexFile.Channel != "" &&
|
||||||
|
channel != indexFile.Channel:
|
||||||
|
return indexFile, ErrIndexChannelMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOldIndexFormat(indexData []byte, channel string) (*IndexFile, error) {
|
||||||
|
releases := make(map[string]string)
|
||||||
|
err := json.Unmarshal(indexData, &releases)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &IndexFile{
|
||||||
|
Channel: channel,
|
||||||
|
Published: time.Now(),
|
||||||
|
Releases: releases,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
55
updater/indexes_test.go
Normal file
55
updater/indexes_test.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
oldFormat = `{
|
||||||
|
"all/ui/modules/assets.zip": "0.3.0",
|
||||||
|
"all/ui/modules/portmaster.zip": "0.2.4",
|
||||||
|
"linux_amd64/core/portmaster-core": "0.8.13"
|
||||||
|
}`
|
||||||
|
|
||||||
|
newFormat = `{
|
||||||
|
"Channel": "stable",
|
||||||
|
"Published": "2022-01-02T00:00:00Z",
|
||||||
|
"Releases": {
|
||||||
|
"all/ui/modules/assets.zip": "0.3.0",
|
||||||
|
"all/ui/modules/portmaster.zip": "0.2.4",
|
||||||
|
"linux_amd64/core/portmaster-core": "0.8.13"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
formatTestChannel = "stable"
|
||||||
|
formatTestReleases = map[string]string{
|
||||||
|
"all/ui/modules/assets.zip": "0.3.0",
|
||||||
|
"all/ui/modules/portmaster.zip": "0.2.4",
|
||||||
|
"linux_amd64/core/portmaster-core": "0.8.13",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIndexParsing(t *testing.T) {
|
||||||
|
lastRelease, err := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldIndexFile, err := ParseIndexFile([]byte(oldFormat), formatTestChannel, lastRelease)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newIndexFile, err := ParseIndexFile([]byte(newFormat), formatTestChannel, lastRelease)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, formatTestChannel, oldIndexFile.Channel, "channel should be the same")
|
||||||
|
assert.Equal(t, formatTestChannel, newIndexFile.Channel, "channel should be the same")
|
||||||
|
assert.Equal(t, formatTestReleases, oldIndexFile.Releases, "releases should be the same")
|
||||||
|
assert.Equal(t, formatTestReleases, newIndexFile.Releases, "releases should be the same")
|
||||||
|
}
|
|
@ -3,7 +3,9 @@ package updater
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
@ -50,6 +52,11 @@ func (reg *ResourceRegistry) AddIndex(idx Index) {
|
||||||
reg.Lock()
|
reg.Lock()
|
||||||
defer reg.Unlock()
|
defer reg.Unlock()
|
||||||
|
|
||||||
|
// Get channel name from path.
|
||||||
|
idx.Channel = strings.TrimSuffix(
|
||||||
|
filepath.Base(idx.Path), filepath.Ext(idx.Path),
|
||||||
|
)
|
||||||
|
|
||||||
reg.indexes = append(reg.indexes, idx)
|
reg.indexes = append(reg.indexes, idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package updater
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -131,18 +130,26 @@ func (reg *ResourceRegistry) loadIndexFile(idx Index) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
releases := make(map[string]string)
|
// Parse the index file.
|
||||||
err = json.Unmarshal(data, &releases)
|
indexFile, err := ParseIndexFile(data, idx.Channel, idx.LastRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(releases) == 0 {
|
// Update last seen release.
|
||||||
log.Debugf("%s: index %s is empty", reg.Name, idx.Path)
|
// TODO: We are working with a copy here, so this has no effect.
|
||||||
|
// This does not break to current implementation, but make the
|
||||||
|
// protection ineffective.
|
||||||
|
idx.LastRelease = indexFile.Published
|
||||||
|
|
||||||
|
// Warn if there aren't any releases in the index.
|
||||||
|
if len(indexFile.Releases) == 0 {
|
||||||
|
log.Debugf("%s: index %s has no releases", reg.Name, idx.Path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = reg.AddResources(releases, false, true, idx.PreRelease)
|
// Add index releases to available resources.
|
||||||
|
err = reg.AddResources(indexFile.Releases, false, true, idx.PreRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("%s: failed to add resource: %s", reg.Name, err)
|
log.Warningf("%s: failed to add resource: %s", reg.Name, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package updater
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -52,23 +51,22 @@ func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Cli
|
||||||
return fmt.Errorf("failed to download index %s: %w", idx.Path, err)
|
return fmt.Errorf("failed to download index %s: %w", idx.Path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse
|
// Parse the index file.
|
||||||
newIndexData := make(map[string]string)
|
indexFile, err := ParseIndexFile(data, idx.Channel, idx.LastRelease)
|
||||||
err = json.Unmarshal(data, &newIndexData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse index %s: %w", idx.Path, err)
|
return fmt.Errorf("failed to parse index %s: %w", idx.Path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add index data to registry.
|
// Add index data to registry.
|
||||||
if len(newIndexData) > 0 {
|
if len(indexFile.Releases) > 0 {
|
||||||
// Check if all resources are within the indexes' authority.
|
// Check if all resources are within the indexes' authority.
|
||||||
authoritativePath := path.Dir(idx.Path) + "/"
|
authoritativePath := path.Dir(idx.Path) + "/"
|
||||||
if authoritativePath == "./" {
|
if authoritativePath == "./" {
|
||||||
// Fix path for indexes at the storage root.
|
// Fix path for indexes at the storage root.
|
||||||
authoritativePath = ""
|
authoritativePath = ""
|
||||||
}
|
}
|
||||||
cleanedData := make(map[string]string, len(newIndexData))
|
cleanedData := make(map[string]string, len(indexFile.Releases))
|
||||||
for key, version := range newIndexData {
|
for key, version := range indexFile.Releases {
|
||||||
if strings.HasPrefix(key, authoritativePath) {
|
if strings.HasPrefix(key, authoritativePath) {
|
||||||
cleanedData[key] = version
|
cleanedData[key] = version
|
||||||
} else {
|
} else {
|
||||||
|
@ -100,7 +98,7 @@ func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Cli
|
||||||
log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err)
|
log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(newIndexData))
|
log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(indexFile.Releases))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue