1package deb
2
3import (
4	"bytes"
5	gocontext "context"
6	"errors"
7	"fmt"
8	"log"
9	"net/url"
10	"os"
11	"path"
12	"sort"
13	"strconv"
14	"strings"
15	"sync"
16	"syscall"
17	"time"
18
19	"github.com/aptly-dev/aptly/aptly"
20	"github.com/aptly-dev/aptly/database"
21	"github.com/aptly-dev/aptly/http"
22	"github.com/aptly-dev/aptly/pgp"
23	"github.com/aptly-dev/aptly/utils"
24	"github.com/pborman/uuid"
25	"github.com/ugorji/go/codec"
26)
27
28// RemoteRepo statuses
29const (
30	MirrorIdle = iota
31	MirrorUpdating
32)
33
34// RemoteRepo represents remote (fetchable) Debian repository.
35//
36// Repostitory could be filtered when fetching by components, architectures
37type RemoteRepo struct {
38	// Permanent internal ID
39	UUID string
40	// User-assigned name
41	Name string
42	// Root of Debian archive, URL
43	ArchiveRoot string
44	// Distribution name, e.g. squeeze
45	Distribution string
46	// List of components to fetch, if empty, then fetch all components
47	Components []string
48	// List of architectures to fetch, if empty, then fetch all architectures
49	Architectures []string
50	// Meta-information about repository
51	Meta Stanza
52	// Last update date
53	LastDownloadDate time.Time
54	// Checksums for release files
55	ReleaseFiles map[string]utils.ChecksumInfo
56	// Filter for packages
57	Filter string
58	// Status marks state of repository (being updated, no action)
59	Status int
60	// WorkerPID is PID of the process modifying the mirror (if any)
61	WorkerPID int
62	// FilterWithDeps to include dependencies from filter query
63	FilterWithDeps bool
64	// SkipComponentCheck skips component list verification
65	SkipComponentCheck bool
66	// SkipArchitectureCheck skips architecture list verification
67	SkipArchitectureCheck bool
68	// Should we download sources?
69	DownloadSources bool
70	// Should we download .udebs?
71	DownloadUdebs bool
72	// Should we download installer files?
73	DownloadInstaller bool
74	// "Snapshot" of current list of packages
75	packageRefs *PackageRefList
76	// Parsed archived root
77	archiveRootURL *url.URL
78	// Current list of packages (filled while updating mirror)
79	packageList *PackageList
80}
81
82// NewRemoteRepo creates new instance of Debian remote repository with specified params
83func NewRemoteRepo(name string, archiveRoot string, distribution string, components []string,
84	architectures []string, downloadSources bool, downloadUdebs bool, downloadInstaller bool) (*RemoteRepo, error) {
85	result := &RemoteRepo{
86		UUID:              uuid.New(),
87		Name:              name,
88		ArchiveRoot:       archiveRoot,
89		Distribution:      distribution,
90		Components:        components,
91		Architectures:     architectures,
92		DownloadSources:   downloadSources,
93		DownloadUdebs:     downloadUdebs,
94		DownloadInstaller: downloadInstaller,
95	}
96
97	err := result.prepare()
98	if err != nil {
99		return nil, err
100	}
101
102	if strings.HasSuffix(result.Distribution, "/") || strings.HasPrefix(result.Distribution, ".") {
103		// flat repo
104		if !strings.HasPrefix(result.Distribution, ".") {
105			result.Distribution = "./" + result.Distribution
106		}
107		result.Architectures = nil
108		if len(result.Components) > 0 {
109			return nil, fmt.Errorf("components aren't supported for flat repos")
110		}
111		if result.DownloadUdebs {
112			return nil, fmt.Errorf("debian-installer udebs aren't supported for flat repos")
113		}
114		result.Components = nil
115	}
116
117	return result, nil
118}
119
120// SetArchiveRoot of remote repo
121func (repo *RemoteRepo) SetArchiveRoot(archiveRoot string) {
122	repo.ArchiveRoot = archiveRoot
123	repo.prepare()
124}
125
126func (repo *RemoteRepo) prepare() error {
127	var err error
128
129	// Add final / to URL
130	if !strings.HasSuffix(repo.ArchiveRoot, "/") {
131		repo.ArchiveRoot = repo.ArchiveRoot + "/"
132	}
133
134	repo.archiveRootURL, err = url.Parse(repo.ArchiveRoot)
135	return err
136}
137
138// String interface
139func (repo *RemoteRepo) String() string {
140	srcFlag := ""
141	if repo.DownloadSources {
142		srcFlag += " [src]"
143	}
144	if repo.DownloadUdebs {
145		srcFlag += " [udeb]"
146	}
147	if repo.DownloadInstaller {
148		srcFlag += " [installer]"
149	}
150	distribution := repo.Distribution
151	if distribution == "" {
152		distribution = "./"
153	}
154	return fmt.Sprintf("[%s]: %s %s%s", repo.Name, repo.ArchiveRoot, distribution, srcFlag)
155}
156
157// IsFlat determines if repository is flat
158func (repo *RemoteRepo) IsFlat() bool {
159	// aptly < 0.5.1 had Distribution = "" for flat repos
160	// aptly >= 0.5.1 had Distribution = "./[path]/" for flat repos
161	return repo.Distribution == "" || (strings.HasPrefix(repo.Distribution, ".") && strings.HasSuffix(repo.Distribution, "/"))
162}
163
164// NumPackages return number of packages retrieved from remote repo
165func (repo *RemoteRepo) NumPackages() int {
166	if repo.packageRefs == nil {
167		return 0
168	}
169	return repo.packageRefs.Len()
170}
171
172// RefList returns package list for repo
173func (repo *RemoteRepo) RefList() *PackageRefList {
174	return repo.packageRefs
175}
176
177// MarkAsUpdating puts current PID and sets status to updating
178func (repo *RemoteRepo) MarkAsUpdating() {
179	repo.Status = MirrorUpdating
180	repo.WorkerPID = os.Getpid()
181}
182
183// MarkAsIdle clears updating flag
184func (repo *RemoteRepo) MarkAsIdle() {
185	repo.Status = MirrorIdle
186	repo.WorkerPID = 0
187}
188
189// CheckLock returns error if mirror is being updated by another process
190func (repo *RemoteRepo) CheckLock() error {
191	if repo.Status == MirrorIdle || repo.WorkerPID == 0 {
192		return nil
193	}
194
195	p, err := os.FindProcess(repo.WorkerPID)
196	if err != nil {
197		return nil
198	}
199
200	err = p.Signal(syscall.Signal(0))
201	if err == nil {
202		return fmt.Errorf("mirror is locked by update operation, PID %d", repo.WorkerPID)
203	}
204
205	return nil
206}
207
208// IndexesRootURL builds URL for various indexes
209func (repo *RemoteRepo) IndexesRootURL() *url.URL {
210	var path *url.URL
211
212	if !repo.IsFlat() {
213		path = &url.URL{Path: fmt.Sprintf("dists/%s/", repo.Distribution)}
214	} else {
215		path = &url.URL{Path: repo.Distribution}
216	}
217
218	return repo.archiveRootURL.ResolveReference(path)
219}
220
221// ReleaseURL returns URL to Release* files in repo root
222func (repo *RemoteRepo) ReleaseURL(name string) *url.URL {
223	return repo.IndexesRootURL().ResolveReference(&url.URL{Path: name})
224}
225
226// FlatBinaryPath returns path to Packages files for flat repo
227func (repo *RemoteRepo) FlatBinaryPath() string {
228	return "Packages"
229}
230
231// FlatSourcesPath returns path to Sources files for flat repo
232func (repo *RemoteRepo) FlatSourcesPath() string {
233	return "Sources"
234}
235
236// BinaryPath returns path to Packages files for given component and
237// architecture
238func (repo *RemoteRepo) BinaryPath(component string, architecture string) string {
239	return fmt.Sprintf("%s/binary-%s/Packages", component, architecture)
240}
241
242// SourcesPath returns path to Sources files for given component
243func (repo *RemoteRepo) SourcesPath(component string) string {
244	return fmt.Sprintf("%s/source/Sources", component)
245}
246
247// UdebPath returns path of Packages files for given component and
248// architecture
249func (repo *RemoteRepo) UdebPath(component string, architecture string) string {
250	return fmt.Sprintf("%s/debian-installer/binary-%s/Packages", component, architecture)
251}
252
253// InstallerPath returns path of Packages files for given component and
254// architecture
255func (repo *RemoteRepo) InstallerPath(component string, architecture string) string {
256	return fmt.Sprintf("%s/installer-%s/current/images/SHA256SUMS", component, architecture)
257}
258
259// PackageURL returns URL of package file relative to repository root
260// architecture
261func (repo *RemoteRepo) PackageURL(filename string) *url.URL {
262	path := &url.URL{Path: filename}
263	return repo.archiveRootURL.ResolveReference(path)
264}
265
266// Fetch updates information about repository
267func (repo *RemoteRepo) Fetch(d aptly.Downloader, verifier pgp.Verifier) error {
268	var (
269		release, inrelease, releasesig *os.File
270		err                            error
271	)
272
273	if verifier == nil {
274		// 0. Just download release file to temporary URL
275		release, err = http.DownloadTemp(gocontext.TODO(), d, repo.ReleaseURL("Release").String())
276		if err != nil {
277			return err
278		}
279	} else {
280		// 1. try InRelease file
281		inrelease, err = http.DownloadTemp(gocontext.TODO(), d, repo.ReleaseURL("InRelease").String())
282		if err != nil {
283			goto splitsignature
284		}
285		defer inrelease.Close()
286
287		_, err = verifier.VerifyClearsigned(inrelease, true)
288		if err != nil {
289			goto splitsignature
290		}
291
292		inrelease.Seek(0, 0)
293
294		release, err = verifier.ExtractClearsigned(inrelease)
295		if err != nil {
296			goto splitsignature
297		}
298
299		goto ok
300
301	splitsignature:
302		// 2. try Release + Release.gpg
303		release, err = http.DownloadTemp(gocontext.TODO(), d, repo.ReleaseURL("Release").String())
304		if err != nil {
305			return err
306		}
307
308		releasesig, err = http.DownloadTemp(gocontext.TODO(), d, repo.ReleaseURL("Release.gpg").String())
309		if err != nil {
310			return err
311		}
312
313		err = verifier.VerifyDetachedSignature(releasesig, release, true)
314		if err != nil {
315			return err
316		}
317
318		_, err = release.Seek(0, 0)
319		if err != nil {
320			return err
321		}
322	}
323ok:
324
325	defer release.Close()
326
327	sreader := NewControlFileReader(release, true, false)
328	stanza, err := sreader.ReadStanza()
329	if err != nil {
330		return err
331	}
332
333	if !repo.IsFlat() {
334		architectures := strings.Split(stanza["Architectures"], " ")
335		sort.Strings(architectures)
336		// "source" architecture is never present, despite Release file claims
337		architectures = utils.StrSlicesSubstract(architectures, []string{ArchitectureSource})
338		if len(repo.Architectures) == 0 {
339			repo.Architectures = architectures
340		} else if !repo.SkipArchitectureCheck {
341			err = utils.StringsIsSubset(repo.Architectures, architectures,
342				fmt.Sprintf("architecture %%s not available in repo %s, use -force-architectures to override", repo))
343			if err != nil {
344				return err
345			}
346		}
347
348		components := strings.Split(stanza["Components"], " ")
349		if strings.Contains(repo.Distribution, "/") {
350			distributionLast := path.Base(repo.Distribution) + "/"
351			for i := range components {
352				components[i] = strings.TrimPrefix(components[i], distributionLast)
353			}
354		}
355		if len(repo.Components) == 0 {
356			repo.Components = components
357		} else if !repo.SkipComponentCheck {
358			err = utils.StringsIsSubset(repo.Components, components,
359				fmt.Sprintf("component %%s not available in repo %s, use -force-components to override", repo))
360			if err != nil {
361				return err
362			}
363		}
364	}
365
366	repo.ReleaseFiles = make(map[string]utils.ChecksumInfo)
367
368	parseSums := func(field string, setter func(sum *utils.ChecksumInfo, data string)) error {
369		for _, line := range strings.Split(stanza[field], "\n") {
370			line = strings.TrimSpace(line)
371			if line == "" {
372				continue
373			}
374			parts := strings.Fields(line)
375
376			if len(parts) != 3 {
377				return fmt.Errorf("unparseable hash sum line: %#v", line)
378			}
379
380			var size int64
381			size, err = strconv.ParseInt(parts[1], 10, 64)
382			if err != nil {
383				return fmt.Errorf("unable to parse size: %s", err)
384			}
385
386			sum := repo.ReleaseFiles[parts[2]]
387
388			sum.Size = size
389			setter(&sum, parts[0])
390
391			repo.ReleaseFiles[parts[2]] = sum
392		}
393
394		delete(stanza, field)
395
396		return nil
397	}
398
399	err = parseSums("MD5Sum", func(sum *utils.ChecksumInfo, data string) { sum.MD5 = data })
400	if err != nil {
401		return err
402	}
403
404	err = parseSums("SHA1", func(sum *utils.ChecksumInfo, data string) { sum.SHA1 = data })
405	if err != nil {
406		return err
407	}
408
409	err = parseSums("SHA256", func(sum *utils.ChecksumInfo, data string) { sum.SHA256 = data })
410	if err != nil {
411		return err
412	}
413
414	err = parseSums("SHA512", func(sum *utils.ChecksumInfo, data string) { sum.SHA512 = data })
415	if err != nil {
416		return err
417	}
418
419	repo.Meta = stanza
420
421	return nil
422}
423
424// DownloadPackageIndexes downloads & parses package index files
425func (repo *RemoteRepo) DownloadPackageIndexes(progress aptly.Progress, d aptly.Downloader, verifier pgp.Verifier, collectionFactory *CollectionFactory,
426	ignoreMismatch bool, maxTries int) error {
427	if repo.packageList != nil {
428		panic("packageList != nil")
429	}
430	repo.packageList = NewPackageList()
431
432	// Download and parse all Packages & Source files
433	packagesPaths := [][]string{}
434
435	if repo.IsFlat() {
436		packagesPaths = append(packagesPaths, []string{repo.FlatBinaryPath(), PackageTypeBinary, "", ""})
437		if repo.DownloadSources {
438			packagesPaths = append(packagesPaths, []string{repo.FlatSourcesPath(), PackageTypeSource, "", ""})
439		}
440	} else {
441		for _, component := range repo.Components {
442			for _, architecture := range repo.Architectures {
443				packagesPaths = append(packagesPaths, []string{repo.BinaryPath(component, architecture), PackageTypeBinary, component, architecture})
444				if repo.DownloadUdebs {
445					packagesPaths = append(packagesPaths, []string{repo.UdebPath(component, architecture), PackageTypeUdeb, component, architecture})
446				}
447				if repo.DownloadInstaller {
448					packagesPaths = append(packagesPaths, []string{repo.InstallerPath(component, architecture), PackageTypeInstaller, component, architecture})
449				}
450			}
451			if repo.DownloadSources {
452				packagesPaths = append(packagesPaths, []string{repo.SourcesPath(component), PackageTypeSource, component, "source"})
453			}
454		}
455	}
456
457	for _, info := range packagesPaths {
458		path, kind, component, architecture := info[0], info[1], info[2], info[3]
459		packagesReader, packagesFile, err := http.DownloadTryCompression(gocontext.TODO(), d, repo.IndexesRootURL(), path, repo.ReleaseFiles, ignoreMismatch, maxTries)
460
461		isInstaller := kind == PackageTypeInstaller
462		if err != nil {
463			if _, ok := err.(*http.NoCandidateFoundError); isInstaller && ok {
464				// checking if gpg file is only needed when checksums matches are required.
465				// otherwise there actually has been no candidate found and we can continue
466				if ignoreMismatch {
467					continue
468				}
469
470				// some repos do not have installer hashsum file listed in release file but provide a separate gpg file
471				hashsumPath := repo.IndexesRootURL().ResolveReference(&url.URL{Path: path}).String()
472				packagesFile, err = http.DownloadTemp(gocontext.TODO(), d, hashsumPath)
473				if err != nil {
474					if herr, ok := err.(*http.Error); ok && (herr.Code == 404 || herr.Code == 403) {
475						// installer files are not available in all components and architectures
476						// so ignore it if not found
477						continue
478					}
479
480					return err
481				}
482
483				if verifier != nil {
484					hashsumGpgPath := repo.IndexesRootURL().ResolveReference(&url.URL{Path: path + ".gpg"}).String()
485					var filesig *os.File
486					filesig, err = http.DownloadTemp(gocontext.TODO(), d, hashsumGpgPath)
487					if err != nil {
488						return err
489					}
490
491					err = verifier.VerifyDetachedSignature(filesig, packagesFile, false)
492					if err != nil {
493						return err
494					}
495
496					_, err = packagesFile.Seek(0, 0)
497				}
498
499				packagesReader = packagesFile
500			}
501
502			if err != nil {
503				return err
504			}
505		}
506		defer packagesFile.Close()
507
508		stat, _ := packagesFile.Stat()
509		progress.InitBar(stat.Size(), true)
510
511		sreader := NewControlFileReader(packagesReader, false, isInstaller)
512
513		for {
514			stanza, err := sreader.ReadStanza()
515			if err != nil {
516				return err
517			}
518			if stanza == nil {
519				break
520			}
521
522			off, _ := packagesFile.Seek(0, 1)
523			progress.SetBar(int(off))
524
525			var p *Package
526
527			if kind == PackageTypeBinary {
528				p = NewPackageFromControlFile(stanza)
529			} else if kind == PackageTypeUdeb {
530				p = NewUdebPackageFromControlFile(stanza)
531			} else if kind == PackageTypeSource {
532				p, err = NewSourcePackageFromControlFile(stanza)
533				if err != nil {
534					return err
535				}
536			} else if kind == PackageTypeInstaller {
537				p, err = NewInstallerPackageFromControlFile(stanza, repo, component, architecture, d)
538				if err != nil {
539					return err
540				}
541			}
542			err = repo.packageList.Add(p)
543			if _, ok := err.(*PackageConflictError); ok {
544				progress.ColoredPrintf("@y[!]@| @!skipping package %s: duplicate in packages index@|", p)
545			} else if err != nil {
546				return err
547			}
548		}
549
550		progress.ShutdownBar()
551	}
552
553	return nil
554}
555
556// ApplyFilter applies filtering to already built PackageList
557func (repo *RemoteRepo) ApplyFilter(dependencyOptions int, filterQuery PackageQuery, progress aptly.Progress) (oldLen, newLen int, err error) {
558	repo.packageList.PrepareIndex()
559
560	emptyList := NewPackageList()
561	emptyList.PrepareIndex()
562
563	oldLen = repo.packageList.Len()
564	repo.packageList, err = repo.packageList.FilterWithProgress([]PackageQuery{filterQuery}, repo.FilterWithDeps, emptyList, dependencyOptions, repo.Architectures, progress)
565	if repo.packageList != nil {
566		newLen = repo.packageList.Len()
567	}
568	return
569}
570
571// BuildDownloadQueue builds queue, discards current PackageList
572func (repo *RemoteRepo) BuildDownloadQueue(packagePool aptly.PackagePool, packageCollection *PackageCollection, checksumStorage aptly.ChecksumStorage, skipExistingPackages bool) (queue []PackageDownloadTask, downloadSize int64, err error) {
573	queue = make([]PackageDownloadTask, 0, repo.packageList.Len())
574	seen := make(map[string]int, repo.packageList.Len())
575
576	err = repo.packageList.ForEach(func(p *Package) error {
577		if repo.packageRefs != nil && skipExistingPackages {
578			if repo.packageRefs.Has(p) {
579				// skip this package, but load checksums/files from package in DB
580				var prevP *Package
581				prevP, err = packageCollection.ByKey(p.Key(""))
582				if err != nil {
583					return err
584				}
585
586				p.UpdateFiles(prevP.Files())
587				return nil
588			}
589		}
590
591		list, err2 := p.DownloadList(packagePool, checksumStorage)
592		if err2 != nil {
593			return err2
594		}
595
596		for _, task := range list {
597			key := task.File.DownloadURL()
598			idx, found := seen[key]
599			if !found {
600				queue = append(queue, task)
601				downloadSize += task.File.Checksums.Size
602				seen[key] = len(queue) - 1
603			} else {
604				// hook up the task to duplicate entry already on the list
605				queue[idx].Additional = append(queue[idx].Additional, task)
606			}
607		}
608
609		return nil
610	})
611	if err != nil {
612		return
613	}
614
615	return
616}
617
618// FinalizeDownload swaps for final value of package refs
619func (repo *RemoteRepo) FinalizeDownload(collectionFactory *CollectionFactory, progress aptly.Progress) error {
620	repo.LastDownloadDate = time.Now()
621
622	if progress != nil {
623		progress.InitBar(int64(repo.packageList.Len()), true)
624	}
625
626	var i int
627
628	// update all the packages in collection
629	err := repo.packageList.ForEach(func(p *Package) error {
630		i++
631		if progress != nil {
632			progress.SetBar(i)
633		}
634		// download process might have updated checksums
635		p.UpdateFiles(p.Files())
636		return collectionFactory.PackageCollection().Update(p)
637	})
638
639	repo.packageRefs = NewPackageRefListFromPackageList(repo.packageList)
640
641	if progress != nil {
642		progress.ShutdownBar()
643	}
644
645	repo.packageList = nil
646
647	return err
648}
649
650// Encode does msgpack encoding of RemoteRepo
651func (repo *RemoteRepo) Encode() []byte {
652	var buf bytes.Buffer
653
654	encoder := codec.NewEncoder(&buf, &codec.MsgpackHandle{})
655	encoder.Encode(repo)
656
657	return buf.Bytes()
658}
659
660// Decode decodes msgpack representation into RemoteRepo
661func (repo *RemoteRepo) Decode(input []byte) error {
662	decoder := codec.NewDecoderBytes(input, &codec.MsgpackHandle{})
663	err := decoder.Decode(repo)
664	if err != nil {
665		if strings.HasPrefix(err.Error(), "codec.decoder: readContainerLen: Unrecognized descriptor byte: hex: 80") {
666			// probably it is broken DB from go < 1.2, try decoding w/o time.Time
667			var repo11 struct { // nolint: maligned
668				UUID             string
669				Name             string
670				ArchiveRoot      string
671				Distribution     string
672				Components       []string
673				Architectures    []string
674				DownloadSources  bool
675				Meta             Stanza
676				LastDownloadDate []byte
677				ReleaseFiles     map[string]utils.ChecksumInfo
678				Filter           string
679				FilterWithDeps   bool
680			}
681
682			decoder = codec.NewDecoderBytes(input, &codec.MsgpackHandle{})
683			err2 := decoder.Decode(&repo11)
684			if err2 != nil {
685				return err
686			}
687
688			repo.UUID = repo11.UUID
689			repo.Name = repo11.Name
690			repo.ArchiveRoot = repo11.ArchiveRoot
691			repo.Distribution = repo11.Distribution
692			repo.Components = repo11.Components
693			repo.Architectures = repo11.Architectures
694			repo.DownloadSources = repo11.DownloadSources
695			repo.Meta = repo11.Meta
696			repo.ReleaseFiles = repo11.ReleaseFiles
697			repo.Filter = repo11.Filter
698			repo.FilterWithDeps = repo11.FilterWithDeps
699		} else {
700			return err
701		}
702	}
703	return repo.prepare()
704}
705
706// Key is a unique id in DB
707func (repo *RemoteRepo) Key() []byte {
708	return []byte("R" + repo.UUID)
709}
710
711// RefKey is a unique id for package reference list
712func (repo *RemoteRepo) RefKey() []byte {
713	return []byte("E" + repo.UUID)
714}
715
716// RemoteRepoCollection does listing, updating/adding/deleting of RemoteRepos
717type RemoteRepoCollection struct {
718	*sync.RWMutex
719	db    database.Storage
720	cache map[string]*RemoteRepo
721}
722
723// NewRemoteRepoCollection loads RemoteRepos from DB and makes up collection
724func NewRemoteRepoCollection(db database.Storage) *RemoteRepoCollection {
725	return &RemoteRepoCollection{
726		RWMutex: &sync.RWMutex{},
727		db:      db,
728		cache:   make(map[string]*RemoteRepo),
729	}
730}
731
732func (collection *RemoteRepoCollection) search(filter func(*RemoteRepo) bool, unique bool) []*RemoteRepo {
733	result := []*RemoteRepo(nil)
734	for _, r := range collection.cache {
735		if filter(r) {
736			result = append(result, r)
737		}
738	}
739
740	if unique && len(result) > 0 {
741		return result
742	}
743
744	collection.db.ProcessByPrefix([]byte("R"), func(key, blob []byte) error {
745		r := &RemoteRepo{}
746		if err := r.Decode(blob); err != nil {
747			log.Printf("Error decoding remote repo: %s\n", err)
748			return nil
749		}
750
751		if filter(r) {
752			if _, exists := collection.cache[r.UUID]; !exists {
753				collection.cache[r.UUID] = r
754				result = append(result, r)
755				if unique {
756					return errors.New("abort")
757				}
758			}
759		}
760
761		return nil
762	})
763
764	return result
765}
766
767// Add appends new repo to collection and saves it
768func (collection *RemoteRepoCollection) Add(repo *RemoteRepo) error {
769	_, err := collection.ByName(repo.Name)
770
771	if err == nil {
772		return fmt.Errorf("mirror with name %s already exists", repo.Name)
773	}
774
775	err = collection.Update(repo)
776	if err != nil {
777		return err
778	}
779
780	collection.cache[repo.UUID] = repo
781	return nil
782}
783
784// Update stores updated information about repo in DB
785func (collection *RemoteRepoCollection) Update(repo *RemoteRepo) error {
786	err := collection.db.Put(repo.Key(), repo.Encode())
787	if err != nil {
788		return err
789	}
790	if repo.packageRefs != nil {
791		err = collection.db.Put(repo.RefKey(), repo.packageRefs.Encode())
792		if err != nil {
793			return err
794		}
795	}
796	return nil
797}
798
799// LoadComplete loads additional information for remote repo
800func (collection *RemoteRepoCollection) LoadComplete(repo *RemoteRepo) error {
801	encoded, err := collection.db.Get(repo.RefKey())
802	if err == database.ErrNotFound {
803		return nil
804	}
805	if err != nil {
806		return err
807	}
808
809	repo.packageRefs = &PackageRefList{}
810	return repo.packageRefs.Decode(encoded)
811}
812
813// ByName looks up repository by name
814func (collection *RemoteRepoCollection) ByName(name string) (*RemoteRepo, error) {
815	result := collection.search(func(r *RemoteRepo) bool { return r.Name == name }, true)
816	if len(result) == 0 {
817		return nil, fmt.Errorf("mirror with name %s not found", name)
818	}
819
820	return result[0], nil
821}
822
823// ByUUID looks up repository by uuid
824func (collection *RemoteRepoCollection) ByUUID(uuid string) (*RemoteRepo, error) {
825	if r, ok := collection.cache[uuid]; ok {
826		return r, nil
827	}
828
829	key := (&RemoteRepo{UUID: uuid}).Key()
830
831	value, err := collection.db.Get(key)
832	if err == database.ErrNotFound {
833		return nil, fmt.Errorf("mirror with uuid %s not found", uuid)
834	}
835	if err != nil {
836		return nil, err
837	}
838
839	r := &RemoteRepo{}
840	err = r.Decode(value)
841
842	if err == nil {
843		collection.cache[r.UUID] = r
844	}
845
846	return r, err
847}
848
849// ForEach runs method for each repository
850func (collection *RemoteRepoCollection) ForEach(handler func(*RemoteRepo) error) error {
851	return collection.db.ProcessByPrefix([]byte("R"), func(key, blob []byte) error {
852		r := &RemoteRepo{}
853		if err := r.Decode(blob); err != nil {
854			log.Printf("Error decoding mirror: %s\n", err)
855			return nil
856		}
857
858		return handler(r)
859	})
860}
861
862// Len returns number of remote repos
863func (collection *RemoteRepoCollection) Len() int {
864	return len(collection.db.KeysByPrefix([]byte("R")))
865}
866
867// Drop removes remote repo from collection
868func (collection *RemoteRepoCollection) Drop(repo *RemoteRepo) error {
869	if _, err := collection.db.Get(repo.Key()); err == database.ErrNotFound {
870		panic("repo not found!")
871	}
872
873	delete(collection.cache, repo.UUID)
874
875	err := collection.db.Delete(repo.Key())
876	if err != nil {
877		return err
878	}
879
880	return collection.db.Delete(repo.RefKey())
881}
882