aptly/deb/snapshot.go

453 lines
12 KiB
Go

package deb
import (
"bytes"
"errors"
"fmt"
"log"
"sort"
"strings"
"time"
"github.com/aptly-dev/aptly/database"
"github.com/aptly-dev/aptly/utils"
"github.com/pborman/uuid"
"github.com/ugorji/go/codec"
)
// Snapshot is immutable state of repository: list of packages
type Snapshot struct {
// Persisten internal ID
UUID string `codec:"UUID" json:"-"`
// Human-readable name
Name string
// Date of creation
CreatedAt time.Time
// Source: kind + ID
SourceKind string `codec:"SourceKind"`
SourceIDs []string `codec:"SourceIDs" json:"-"`
// Sources
Snapshots []*Snapshot `codec:"-" json:",omitempty"`
RemoteRepos []*RemoteRepo `codec:"-" json:",omitempty"`
LocalRepos []*LocalRepo `codec:"-" json:",omitempty"`
Packages []string `codec:"-" json:",omitempty"`
// Description of how snapshot was created
Description string
Origin string
NotAutomatic string
ButAutomaticUpgrades string
packageRefs *PackageRefList
}
// NewSnapshotFromRepository creates snapshot from current state of repository
func NewSnapshotFromRepository(name string, repo *RemoteRepo) (*Snapshot, error) {
if repo.packageRefs == nil || repo.packageRefs.Len() == 0 {
return nil, errors.New("mirror not updated")
}
return &Snapshot{
UUID: uuid.New(),
Name: name,
CreatedAt: time.Now(),
SourceKind: SourceRemoteRepo,
SourceIDs: []string{repo.UUID},
Description: fmt.Sprintf("Snapshot from mirror %s", repo),
Origin: repo.Meta["Origin"],
NotAutomatic: repo.Meta["NotAutomatic"],
ButAutomaticUpgrades: repo.Meta["ButAutomaticUpgrades"],
packageRefs: repo.packageRefs,
}, nil
}
// NewSnapshotFromLocalRepo creates snapshot from current state of local repository
func NewSnapshotFromLocalRepo(name string, repo *LocalRepo) (*Snapshot, error) {
snap := &Snapshot{
UUID: uuid.New(),
Name: name,
CreatedAt: time.Now(),
SourceKind: SourceLocalRepo,
SourceIDs: []string{repo.UUID},
Description: fmt.Sprintf("Snapshot from local repo %s", repo),
packageRefs: repo.packageRefs,
}
if snap.packageRefs == nil {
snap.packageRefs = NewPackageRefList()
}
return snap, nil
}
// NewSnapshotFromPackageList creates snapshot from PackageList
func NewSnapshotFromPackageList(name string, sources []*Snapshot, list *PackageList, description string) *Snapshot {
return NewSnapshotFromRefList(name, sources, NewPackageRefListFromPackageList(list), description)
}
// NewSnapshotFromRefList creates snapshot from PackageRefList
func NewSnapshotFromRefList(name string, sources []*Snapshot, list *PackageRefList, description string) *Snapshot {
sourceUUIDs := make([]string, len(sources))
for i := range sources {
sourceUUIDs[i] = sources[i].UUID
}
return &Snapshot{
UUID: uuid.New(),
Name: name,
CreatedAt: time.Now(),
SourceKind: "snapshot",
SourceIDs: sourceUUIDs,
Description: description,
packageRefs: list,
}
}
// String returns string representation of snapshot
func (s *Snapshot) String() string {
return fmt.Sprintf("[%s]: %s", s.Name, s.Description)
}
// NumPackages returns number of packages in snapshot
func (s *Snapshot) NumPackages() int {
return s.packageRefs.Len()
}
// RefList returns list of package refs in snapshot
func (s *Snapshot) RefList() *PackageRefList {
return s.packageRefs
}
// Key is a unique id in DB
func (s *Snapshot) Key() []byte {
return []byte("S" + s.UUID)
}
// ResourceKey is a unique identifier of the resource
// this snapshot uses. Instead of uuid it uses name
// which needs to be unique as well.
func (s *Snapshot) ResourceKey() []byte {
return []byte("S" + s.Name)
}
// RefKey is a unique id for package reference list
func (s *Snapshot) RefKey() []byte {
return []byte("E" + s.UUID)
}
// Encode does msgpack encoding of Snapshot
func (s *Snapshot) Encode() []byte {
var buf bytes.Buffer
encoder := codec.NewEncoder(&buf, &codec.MsgpackHandle{})
encoder.Encode(s)
return buf.Bytes()
}
// Decode decodes msgpack representation into Snapshot
func (s *Snapshot) Decode(input []byte) error {
decoder := codec.NewDecoderBytes(input, &codec.MsgpackHandle{})
err := decoder.Decode(s)
if err != nil {
if strings.HasPrefix(err.Error(), "codec.decoder: readContainerLen: Unrecognized descriptor byte: hex: 80") {
// probably it is broken DB from go < 1.2, try decoding w/o time.Time
var snapshot11 struct {
UUID string
Name string
CreatedAt []byte
SourceKind string
SourceIDs []string
Description string
}
decoder = codec.NewDecoderBytes(input, &codec.MsgpackHandle{})
err2 := decoder.Decode(&snapshot11)
if err2 != nil {
return err
}
s.UUID = snapshot11.UUID
s.Name = snapshot11.Name
s.SourceKind = snapshot11.SourceKind
s.SourceIDs = snapshot11.SourceIDs
s.Description = snapshot11.Description
} else if strings.Contains(err.Error(), "invalid length of bytes for decoding time") {
// DB created by old codec version, time.Time is not builtin type.
// https://github.com/ugorji/go-codec/issues/269
decoder := codec.NewDecoderBytes(input, &codec.MsgpackHandle{
// only can be configured in Deprecated BasicHandle struct
BasicHandle: codec.BasicHandle{ // nolint: staticcheck
TimeNotBuiltin: true,
},
})
if err = decoder.Decode(s); err != nil {
return err
}
} else {
return err
}
}
return nil
}
// SnapshotCollection does listing, updating/adding/deleting of Snapshots
type SnapshotCollection struct {
db database.Storage
cache map[string]*Snapshot
}
// NewSnapshotCollection loads Snapshots from DB and makes up collection
func NewSnapshotCollection(db database.Storage) *SnapshotCollection {
return &SnapshotCollection{
db: db,
cache: map[string]*Snapshot{},
}
}
// Add appends new repo to collection and saves it
func (collection *SnapshotCollection) Add(snapshot *Snapshot) error {
_, err := collection.ByName(snapshot.Name)
if err == nil {
return fmt.Errorf("snapshot with name %s already exists", snapshot.Name)
}
err = collection.Update(snapshot)
if err != nil {
return err
}
collection.cache[snapshot.UUID] = snapshot
return nil
}
// Update stores updated information about snapshot in DB
func (collection *SnapshotCollection) Update(snapshot *Snapshot) error {
batch := collection.db.CreateBatch()
batch.Put(snapshot.Key(), snapshot.Encode())
if snapshot.packageRefs != nil {
batch.Put(snapshot.RefKey(), snapshot.packageRefs.Encode())
}
return batch.Write()
}
// LoadComplete loads additional information about snapshot
func (collection *SnapshotCollection) LoadComplete(snapshot *Snapshot) error {
encoded, err := collection.db.Get(snapshot.RefKey())
if err != nil {
return err
}
snapshot.packageRefs = &PackageRefList{}
return snapshot.packageRefs.Decode(encoded)
}
func (collection *SnapshotCollection) search(filter func(*Snapshot) bool, unique bool) []*Snapshot {
result := []*Snapshot(nil)
for _, s := range collection.cache {
if filter(s) {
result = append(result, s)
}
}
if unique && len(result) > 0 {
return result
}
collection.db.ProcessByPrefix([]byte("S"), func(_, blob []byte) error {
s := &Snapshot{}
if err := s.Decode(blob); err != nil {
log.Printf("Error decoding snapshot: %s\n", err)
return nil
}
if filter(s) {
if _, exists := collection.cache[s.UUID]; !exists {
collection.cache[s.UUID] = s
result = append(result, s)
if unique {
return errors.New("abort")
}
}
}
return nil
})
return result
}
// ByName looks up snapshot by name
func (collection *SnapshotCollection) ByName(name string) (*Snapshot, error) {
result := collection.search(func(s *Snapshot) bool { return s.Name == name }, true)
if len(result) > 0 {
return result[0], nil
}
return nil, fmt.Errorf("snapshot with name %s not found", name)
}
// ByUUID looks up snapshot by UUID
func (collection *SnapshotCollection) ByUUID(uuid string) (*Snapshot, error) {
if s, ok := collection.cache[uuid]; ok {
return s, nil
}
key := (&Snapshot{UUID: uuid}).Key()
value, err := collection.db.Get(key)
if err == database.ErrNotFound {
return nil, fmt.Errorf("snapshot with uuid %s not found", uuid)
}
if err != nil {
return nil, err
}
s := &Snapshot{}
err = s.Decode(value)
if err == nil {
collection.cache[s.UUID] = s
}
return s, err
}
// ByRemoteRepoSource looks up snapshots that have specified RemoteRepo as a source
func (collection *SnapshotCollection) ByRemoteRepoSource(repo *RemoteRepo) []*Snapshot {
return collection.search(func(s *Snapshot) bool {
return s.SourceKind == SourceRemoteRepo && utils.StrSliceHasItem(s.SourceIDs, repo.UUID)
}, false)
}
// ByLocalRepoSource looks up snapshots that have specified LocalRepo as a source
func (collection *SnapshotCollection) ByLocalRepoSource(repo *LocalRepo) []*Snapshot {
return collection.search(func(s *Snapshot) bool {
return s.SourceKind == SourceLocalRepo && utils.StrSliceHasItem(s.SourceIDs, repo.UUID)
}, false)
}
// BySnapshotSource looks up snapshots that have specified snapshot as a source
func (collection *SnapshotCollection) BySnapshotSource(snapshot *Snapshot) []*Snapshot {
return collection.search(func(s *Snapshot) bool {
return s.SourceKind == "snapshot" && utils.StrSliceHasItem(s.SourceIDs, snapshot.UUID)
}, false)
}
// ForEach runs method for each snapshot
func (collection *SnapshotCollection) ForEach(handler func(*Snapshot) error) error {
return collection.db.ProcessByPrefix([]byte("S"), func(_, blob []byte) error {
s := &Snapshot{}
if err := s.Decode(blob); err != nil {
log.Printf("Error decoding snapshot: %s\n", err)
return nil
}
return handler(s)
})
}
// ForEachSorted runs method for each snapshot following some sort order
func (collection *SnapshotCollection) ForEachSorted(sortMethod string, handler func(*Snapshot) error) error {
blobs := collection.db.FetchByPrefix([]byte("S"))
list := make([]*Snapshot, 0, len(blobs))
for _, blob := range blobs {
s := &Snapshot{}
if err := s.Decode(blob); err != nil {
log.Printf("Error decoding snapshot: %s\n", err)
} else {
list = append(list, s)
}
}
sorter, err := newSnapshotSorter(sortMethod, list)
if err != nil {
return err
}
for _, s := range sorter.list {
err = handler(s)
if err != nil {
return err
}
}
return nil
}
// Len returns number of snapshots in collection
// ForEach runs method for each snapshot
func (collection *SnapshotCollection) Len() int {
return len(collection.db.KeysByPrefix([]byte("S")))
}
// Drop removes snapshot from collection
func (collection *SnapshotCollection) Drop(snapshot *Snapshot) error {
if _, err := collection.db.Get(snapshot.Key()); err != nil {
if err == database.ErrNotFound {
return errors.New("snapshot not found")
}
return err
}
delete(collection.cache, snapshot.UUID)
batch := collection.db.CreateBatch()
batch.Delete(snapshot.Key())
batch.Delete(snapshot.RefKey())
return batch.Write()
}
// Snapshot sorting methods
const (
SortName = iota
SortTime
)
type snapshotSorter struct {
list []*Snapshot
sortMethod int
}
func newSnapshotSorter(sortMethod string, list []*Snapshot) (*snapshotSorter, error) {
s := &snapshotSorter{list: list}
switch sortMethod {
case "time", "Time":
s.sortMethod = SortTime
case "name", "Name":
s.sortMethod = SortName
default:
return nil, fmt.Errorf("sorting method \"%s\" unknown", sortMethod)
}
sort.Sort(s)
return s, nil
}
func (s *snapshotSorter) Swap(i, j int) {
s.list[i], s.list[j] = s.list[j], s.list[i]
}
func (s *snapshotSorter) Less(i, j int) bool {
switch s.sortMethod {
case SortName:
return s.list[i].Name < s.list[j].Name
case SortTime:
return s.list[i].CreatedAt.Before(s.list[j].CreatedAt)
}
panic("unknown sort method")
}
func (s *snapshotSorter) Len() int {
return len(s.list)
}