aptly/debian/remote.go

563 lines
13 KiB
Go

// Package debian implements Debian-specific repository handling
package debian
import (
"bytes"
"code.google.com/p/go-uuid/uuid"
"fmt"
"github.com/smira/aptly/database"
"github.com/smira/aptly/utils"
"github.com/ugorji/go/codec"
"log"
"net/url"
"os"
"strconv"
"strings"
"time"
)
// RemoteRepo represents remote (fetchable) Debian repository.
//
// Repostitory could be filtered when fetching by components, architectures
// TODO: support flat format
type RemoteRepo struct {
// Permanent internal ID
UUID string
// User-assigned name
Name string
// Root of Debian archive, URL
ArchiveRoot string
// Distribution name, e.g. squeeze
Distribution string
// List of components to fetch, if empty, then fetch all components
Components []string
// List of architectures to fetch, if empty, then fetch all architectures
Architectures []string
// Meta-information about repository
Meta Stanza
// Last update date
LastDownloadDate time.Time
// Checksums for release files
ReleaseFiles map[string]utils.ChecksumInfo
// "Snapshot" of current list of packages
packageRefs *PackageRefList
// Parsed archived root
archiveRootURL *url.URL
}
// NewRemoteRepo creates new instance of Debian remote repository with specified params
func NewRemoteRepo(name string, archiveRoot string, distribution string, components []string, architectures []string) (*RemoteRepo, error) {
result := &RemoteRepo{
UUID: uuid.New(),
Name: name,
ArchiveRoot: archiveRoot,
Distribution: distribution,
Components: components,
Architectures: architectures,
}
err := result.prepare()
if err != nil {
return nil, err
}
if result.Distribution == "." || result.Distribution == "./" {
// flat repo
result.Distribution = ""
result.Architectures = nil
if len(result.Components) > 0 {
return nil, fmt.Errorf("components aren't supported for flat repos")
}
result.Components = nil
}
return result, nil
}
func (repo *RemoteRepo) prepare() error {
var err error
repo.archiveRootURL, err = url.Parse(repo.ArchiveRoot)
return err
}
// String interface
func (repo *RemoteRepo) String() string {
return fmt.Sprintf("[%s]: %s %s", repo.Name, repo.ArchiveRoot, repo.Distribution)
}
// IsFlat determines if repository is flat
func (repo *RemoteRepo) IsFlat() bool {
return repo.Distribution == ""
}
// NumPackages return number of packages retrived from remore repo
func (repo *RemoteRepo) NumPackages() int {
if repo.packageRefs == nil {
return 0
}
return repo.packageRefs.Len()
}
// RefList returns package list for repo
func (repo *RemoteRepo) RefList() *PackageRefList {
return repo.packageRefs
}
// ReleaseURL returns URL to Release* files in repo root
func (repo *RemoteRepo) ReleaseURL(name string) *url.URL {
var path *url.URL
if !repo.IsFlat() {
path = &url.URL{Path: fmt.Sprintf("dists/%s/%s", repo.Distribution, name)}
} else {
path = &url.URL{Path: name}
}
return repo.archiveRootURL.ResolveReference(path)
}
// FlatBinaryURL returns URL to Package files for flat repo
func (repo *RemoteRepo) FlatBinaryURL() *url.URL {
path := &url.URL{Path: "Packages"}
return repo.archiveRootURL.ResolveReference(path)
}
// BinaryURL returns URL of Packages file for given component and
// architecture
func (repo *RemoteRepo) BinaryURL(component string, architecture string) *url.URL {
path := &url.URL{Path: fmt.Sprintf("dists/%s/%s/binary-%s/Packages", repo.Distribution, component, architecture)}
return repo.archiveRootURL.ResolveReference(path)
}
// PackageURL returns URL of package file relative to repository root
// architecture
func (repo *RemoteRepo) PackageURL(filename string) *url.URL {
path := &url.URL{Path: filename}
return repo.archiveRootURL.ResolveReference(path)
}
// Fetch updates information about repository
func (repo *RemoteRepo) Fetch(d utils.Downloader, verifier utils.Verifier) error {
var (
release *os.File
err error
)
if verifier == nil {
// 0. Just download release file to temporary URL
release, err = utils.DownloadTemp(d, repo.ReleaseURL("Release").String())
if err != nil {
return err
}
} else {
// 1. try InRelease file
inrelease, err := utils.DownloadTemp(d, repo.ReleaseURL("InRelease").String())
if err != nil {
goto splitsignature
}
defer inrelease.Close()
release, err = verifier.VerifyClearsigned(inrelease)
if err != nil {
goto splitsignature
}
goto ok
splitsignature:
// 2. try Release + Release.gpg
release, err = utils.DownloadTemp(d, repo.ReleaseURL("Release").String())
if err != nil {
return err
}
releasesig, err := utils.DownloadTemp(d, repo.ReleaseURL("Release.gpg").String())
if err != nil {
return err
}
err = verifier.VerifyDetachedSignature(releasesig, release)
if err != nil {
return err
}
_, err = release.Seek(0, 0)
if err != nil {
return err
}
}
ok:
defer release.Close()
sreader := NewControlFileReader(release)
stanza, err := sreader.ReadStanza()
if err != nil {
return err
}
if !repo.IsFlat() {
architectures := strings.Split(stanza["Architectures"], " ")
if len(repo.Architectures) == 0 {
repo.Architectures = architectures
} else {
err = utils.StringsIsSubset(repo.Architectures, architectures,
fmt.Sprintf("architecture %%s not available in repo %s", repo))
if err != nil {
return err
}
}
components := strings.Split(stanza["Components"], " ")
if len(repo.Components) == 0 {
repo.Components = components
} else {
err = utils.StringsIsSubset(repo.Components, components,
fmt.Sprintf("component %%s not available in repo %s", repo))
if err != nil {
return err
}
}
}
repo.ReleaseFiles = make(map[string]utils.ChecksumInfo)
parseSums := func(field string, setter func(sum *utils.ChecksumInfo, data string)) error {
for _, line := range strings.Split(stanza[field], "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) != 3 {
return fmt.Errorf("unparseable hash sum line: %#v", line)
}
size, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return fmt.Errorf("unable to parse size: %s", err)
}
sum := repo.ReleaseFiles[parts[2]]
sum.Size = size
setter(&sum, parts[0])
repo.ReleaseFiles[parts[2]] = sum
}
delete(stanza, field)
return nil
}
err = parseSums("MD5Sum", func(sum *utils.ChecksumInfo, data string) { sum.MD5 = data })
if err != nil {
return err
}
err = parseSums("SHA1", func(sum *utils.ChecksumInfo, data string) { sum.SHA1 = data })
if err != nil {
return err
}
err = parseSums("SHA256", func(sum *utils.ChecksumInfo, data string) { sum.SHA256 = data })
if err != nil {
return err
}
repo.Meta = stanza
return nil
}
// Download downloads all repo files
func (repo *RemoteRepo) Download(d utils.Downloader, packageCollection *PackageCollection, packageRepo *Repository, ignoreMismatch bool) error {
list := NewPackageList()
d.GetProgress().Printf("Downloading & parsing package files...\n")
// Download and parse all Packages files
packagesURLs := []string{}
if repo.IsFlat() {
packagesURLs = append(packagesURLs, repo.FlatBinaryURL().String())
} else {
for _, component := range repo.Components {
for _, architecture := range repo.Architectures {
packagesURLs = append(packagesURLs, repo.BinaryURL(component, architecture).String())
}
}
}
for _, url := range packagesURLs {
packagesReader, packagesFile, err := utils.DownloadTryCompression(d, url, repo.ReleaseFiles, ignoreMismatch)
if err != nil {
return err
}
defer packagesFile.Close()
sreader := NewControlFileReader(packagesReader)
for {
stanza, err := sreader.ReadStanza()
if err != nil {
return err
}
if stanza == nil {
break
}
p := NewPackageFromControlFile(stanza)
list.Add(p)
}
}
d.GetProgress().Printf("Saving packages to database...\n")
d.GetProgress().InitBar(int64(list.Len()), false)
// Save package meta information to DB
err := list.ForEach(func(p *Package) error {
d.GetProgress().AddBar(1)
return packageCollection.Update(p)
})
if err != nil {
return fmt.Errorf("unable to save packages to db: %s", err)
}
d.GetProgress().ShutdownBar()
d.GetProgress().Printf("Building download queue...\n")
// Build download queue
queued := make(map[string]PackageDownloadTask, list.Len())
count := 0
downloadSize := int64(0)
err = list.ForEach(func(p *Package) error {
list, err := p.DownloadList(packageRepo)
if err != nil {
return err
}
for _, task := range list {
key := task.RepoURI + "-" + task.DestinationPath
_, found := queued[key]
if !found {
count++
downloadSize += task.Checksums.Size
queued[key] = task
}
}
return nil
})
if err != nil {
return fmt.Errorf("unable to build download queue: %s", err)
}
d.GetProgress().Printf("Download queue: %d items, %.2f GiB size\n", count, float64(downloadSize)/(1024.0*1024.0*1024.0))
d.GetProgress().InitBar(downloadSize, true)
// Download all package files
ch := make(chan error, len(queued))
for _, task := range queued {
d.DownloadWithChecksum(repo.PackageURL(task.RepoURI).String(), task.DestinationPath, ch, task.Checksums, ignoreMismatch)
}
// Wait for all downloads to finish
errors := make([]string, 0)
for count > 0 {
err = <-ch
if err != nil {
errors = append(errors, err.Error())
}
count--
}
d.GetProgress().ShutdownBar()
if len(errors) > 0 {
return fmt.Errorf("download errors:\n %s\n", strings.Join(errors, "\n "))
}
repo.LastDownloadDate = time.Now()
repo.packageRefs = NewPackageRefListFromPackageList(list)
return nil
}
// Encode does msgpack encoding of RemoteRepo
func (repo *RemoteRepo) Encode() []byte {
var buf bytes.Buffer
encoder := codec.NewEncoder(&buf, &codec.MsgpackHandle{})
encoder.Encode(repo)
return buf.Bytes()
}
// Decode decodes msgpack representation into RemoteRepo
func (repo *RemoteRepo) Decode(input []byte) error {
decoder := codec.NewDecoderBytes(input, &codec.MsgpackHandle{})
err := decoder.Decode(repo)
if err != nil {
return err
}
return repo.prepare()
}
// Key is a unique id in DB
func (repo *RemoteRepo) Key() []byte {
return []byte("R" + repo.UUID)
}
// RefKey is a unique id for package reference list
func (repo *RemoteRepo) RefKey() []byte {
return []byte("E" + repo.UUID)
}
// RemoteRepoCollection does listing, updating/adding/deleting of RemoteRepos
type RemoteRepoCollection struct {
db database.Storage
list []*RemoteRepo
}
// NewRemoteRepoCollection loads RemoteRepos from DB and makes up collection
func NewRemoteRepoCollection(db database.Storage) *RemoteRepoCollection {
result := &RemoteRepoCollection{
db: db,
}
blobs := db.FetchByPrefix([]byte("R"))
result.list = make([]*RemoteRepo, 0, len(blobs))
for _, blob := range blobs {
r := &RemoteRepo{}
if err := r.Decode(blob); err != nil {
log.Printf("Error decoding mirror: %s\n", err)
} else {
result.list = append(result.list, r)
}
}
return result
}
// Add appends new repo to collection and saves it
func (collection *RemoteRepoCollection) Add(repo *RemoteRepo) error {
for _, r := range collection.list {
if r.Name == repo.Name {
return fmt.Errorf("mirror with name %s already exists", repo.Name)
}
}
err := collection.Update(repo)
if err != nil {
return err
}
collection.list = append(collection.list, repo)
return nil
}
// Update stores updated information about repo in DB
func (collection *RemoteRepoCollection) Update(repo *RemoteRepo) error {
err := collection.db.Put(repo.Key(), repo.Encode())
if err != nil {
return err
}
if repo.packageRefs != nil {
err = collection.db.Put(repo.RefKey(), repo.packageRefs.Encode())
if err != nil {
return err
}
}
return nil
}
// LoadComplete loads additional information for remote repo
func (collection *RemoteRepoCollection) LoadComplete(repo *RemoteRepo) error {
encoded, err := collection.db.Get(repo.RefKey())
if err == database.ErrNotFound {
return nil
}
if err != nil {
return err
}
repo.packageRefs = &PackageRefList{}
return repo.packageRefs.Decode(encoded)
}
// ByName looks up repository by name
func (collection *RemoteRepoCollection) ByName(name string) (*RemoteRepo, error) {
for _, r := range collection.list {
if r.Name == name {
return r, nil
}
}
return nil, fmt.Errorf("mirror with name %s not found", name)
}
// ByUUID looks up repository by uuid
func (collection *RemoteRepoCollection) ByUUID(uuid string) (*RemoteRepo, error) {
for _, r := range collection.list {
if r.UUID == uuid {
return r, nil
}
}
return nil, fmt.Errorf("mirror with uuid %s not found", uuid)
}
// ForEach runs method for each repository
func (collection *RemoteRepoCollection) ForEach(handler func(*RemoteRepo) error) error {
var err error
for _, r := range collection.list {
err = handler(r)
if err != nil {
return err
}
}
return err
}
// Len returns number of remote repos
func (collection *RemoteRepoCollection) Len() int {
return len(collection.list)
}
// Drop removes remote repo from collection
func (collection *RemoteRepoCollection) Drop(repo *RemoteRepo) error {
repoPosition := -1
for i, r := range collection.list {
if r == repo {
repoPosition = i
break
}
}
if repoPosition == -1 {
panic("repo not found!")
}
collection.list[len(collection.list)-1], collection.list[repoPosition], collection.list =
nil, collection.list[len(collection.list)-1], collection.list[:len(collection.list)-1]
err := collection.db.Delete(repo.Key())
if err != nil {
return err
}
return collection.db.Delete(repo.RefKey())
}