mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-05-07 22:20:24 +00:00
Published persistence: save persisted when publishing.
This commit is contained in:
+17
-1
@@ -21,10 +21,15 @@ func aptlyPublishSnapshot(cmd *commander.Command, args []string) error {
|
|||||||
var prefix string
|
var prefix string
|
||||||
if len(args) == 2 {
|
if len(args) == 2 {
|
||||||
prefix = args[1]
|
prefix = args[1]
|
||||||
|
if prefix == "." || prefix == "/" {
|
||||||
|
prefix = ""
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
prefix = ""
|
prefix = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publishedCollecton := debian.NewPublishedRepoCollection(context.database)
|
||||||
|
|
||||||
snapshotCollection := debian.NewSnapshotCollection(context.database)
|
snapshotCollection := debian.NewSnapshotCollection(context.database)
|
||||||
snapshot, err := snapshotCollection.ByName(name)
|
snapshot, err := snapshotCollection.ByName(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -67,10 +72,21 @@ func aptlyPublishSnapshot(cmd *commander.Command, args []string) error {
|
|||||||
|
|
||||||
published := debian.NewPublishedRepo(prefix, distribution, component, context.architecturesList, snapshot)
|
published := debian.NewPublishedRepo(prefix, distribution, component, context.architecturesList, snapshot)
|
||||||
|
|
||||||
|
duplicate := publishedCollecton.CheckDuplicate(published)
|
||||||
|
if duplicate != nil {
|
||||||
|
publishedCollecton.LoadComplete(duplicate, snapshotCollection)
|
||||||
|
return fmt.Errorf("prefix/distribution already used by another published repo: %s", duplicate)
|
||||||
|
}
|
||||||
|
|
||||||
packageCollection := debian.NewPackageCollection(context.database)
|
packageCollection := debian.NewPackageCollection(context.database)
|
||||||
err = published.Publish(context.packageRepository, packageCollection, signer)
|
err = published.Publish(context.packageRepository, packageCollection, signer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("unable to publish: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = publishedCollecton.Add(published)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to save to DB: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if prefix != "" && !strings.HasSuffix(prefix, "/") {
|
if prefix != "" && !strings.HasSuffix(prefix, "/") {
|
||||||
|
|||||||
Vendored
+157
-1
@@ -2,8 +2,13 @@ package debian
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"code.google.com/p/go-uuid/uuid"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/smira/aptly/database"
|
||||||
"github.com/smira/aptly/utils"
|
"github.com/smira/aptly/utils"
|
||||||
|
"github.com/ugorji/go/codec"
|
||||||
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -11,6 +16,8 @@ import (
|
|||||||
|
|
||||||
// PublishedRepo is a published for http/ftp representation of snapshot as Debian repository
|
// PublishedRepo is a published for http/ftp representation of snapshot as Debian repository
|
||||||
type PublishedRepo struct {
|
type PublishedRepo struct {
|
||||||
|
// Internal unique ID
|
||||||
|
UUID string
|
||||||
// Prefix & distribution should be unique across all published repositories
|
// Prefix & distribution should be unique across all published repositories
|
||||||
Prefix string
|
Prefix string
|
||||||
Distribution string
|
Distribution string
|
||||||
@@ -26,6 +33,7 @@ type PublishedRepo struct {
|
|||||||
// NewPublishedRepo creates new published repository
|
// NewPublishedRepo creates new published repository
|
||||||
func NewPublishedRepo(prefix string, distribution string, component string, architectures []string, snapshot *Snapshot) *PublishedRepo {
|
func NewPublishedRepo(prefix string, distribution string, component string, architectures []string, snapshot *Snapshot) *PublishedRepo {
|
||||||
return &PublishedRepo{
|
return &PublishedRepo{
|
||||||
|
UUID: uuid.New(),
|
||||||
Prefix: prefix,
|
Prefix: prefix,
|
||||||
Distribution: distribution,
|
Distribution: distribution,
|
||||||
Component: component,
|
Component: component,
|
||||||
@@ -35,6 +43,44 @@ func NewPublishedRepo(prefix string, distribution string, component string, arch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns human-readable represenation of PublishedRepo
|
||||||
|
func (p *PublishedRepo) String() string {
|
||||||
|
var prefix, archs string
|
||||||
|
|
||||||
|
if p.Prefix != "" {
|
||||||
|
prefix = p.Prefix
|
||||||
|
} else {
|
||||||
|
prefix = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.Architectures) > 0 {
|
||||||
|
archs = fmt.Sprintf(" [%s]", strings.Join(p.Architectures, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s (%s)%s publishes %s", prefix, p.Distribution, p.Component, archs, p.snapshot.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key returns unique key identifying PublishedRepo
|
||||||
|
func (p *PublishedRepo) Key() []byte {
|
||||||
|
return []byte("U" + p.Prefix + ">>" + p.Distribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode does msgpack encoding of PublishedRepo
|
||||||
|
func (p *PublishedRepo) Encode() []byte {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
encoder := codec.NewEncoder(&buf, &codec.MsgpackHandle{})
|
||||||
|
encoder.Encode(p)
|
||||||
|
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes msgpack representation into PublishedRepo
|
||||||
|
func (p *PublishedRepo) Decode(input []byte) error {
|
||||||
|
decoder := codec.NewDecoderBytes(input, &codec.MsgpackHandle{})
|
||||||
|
return decoder.Decode(p)
|
||||||
|
}
|
||||||
|
|
||||||
// Publish publishes snapshot (repository) contents, links package files, generates Packages & Release files, signs them
|
// Publish publishes snapshot (repository) contents, links package files, generates Packages & Release files, signs them
|
||||||
func (p *PublishedRepo) Publish(repo *Repository, packageCollection *PackageCollection, signer utils.Signer) error {
|
func (p *PublishedRepo) Publish(repo *Repository, packageCollection *PackageCollection, signer utils.Signer) error {
|
||||||
err := repo.MkDir(filepath.Join(p.Prefix, "pool"))
|
err := repo.MkDir(filepath.Join(p.Prefix, "pool"))
|
||||||
@@ -57,7 +103,7 @@ func (p *PublishedRepo) Publish(repo *Repository, packageCollection *PackageColl
|
|||||||
return fmt.Errorf("repository is empty, can't publish")
|
return fmt.Errorf("repository is empty, can't publish")
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Architectures == nil {
|
if len(p.Architectures) == 0 {
|
||||||
p.Architectures = list.Architectures()
|
p.Architectures = list.Architectures()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,3 +235,113 @@ func (p *PublishedRepo) Publish(repo *Repository, packageCollection *PackageColl
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishedRepoCollection does listing, updating/adding/deleting of PublishedRepos
|
||||||
|
type PublishedRepoCollection struct {
|
||||||
|
db database.Storage
|
||||||
|
list []*PublishedRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPublishedRepoCollection loads PublishedRepos from DB and makes up collection
|
||||||
|
func NewPublishedRepoCollection(db database.Storage) *PublishedRepoCollection {
|
||||||
|
result := &PublishedRepoCollection{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
|
||||||
|
blobs := db.FetchByPrefix([]byte("U"))
|
||||||
|
result.list = make([]*PublishedRepo, 0, len(blobs))
|
||||||
|
|
||||||
|
for _, blob := range blobs {
|
||||||
|
r := &PublishedRepo{}
|
||||||
|
if err := r.Decode(blob); err != nil {
|
||||||
|
log.Printf("Error decoding published repo: %s\n", err)
|
||||||
|
} else {
|
||||||
|
result.list = append(result.list, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add appends new repo to collection and saves it
|
||||||
|
func (collection *PublishedRepoCollection) Add(repo *PublishedRepo) error {
|
||||||
|
if collection.CheckDuplicate(repo) != nil {
|
||||||
|
return fmt.Errorf("published repo with prefix/distribution %s/%s already exists", repo.Prefix, repo.Distribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := collection.Update(repo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
collection.list = append(collection.list, repo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckDuplicate verifies that there's no published repo with the same name
|
||||||
|
func (collection *PublishedRepoCollection) CheckDuplicate(repo *PublishedRepo) *PublishedRepo {
|
||||||
|
for _, r := range collection.list {
|
||||||
|
if r.Prefix == repo.Prefix && r.Distribution == repo.Distribution {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stores updated information about repo in DB
|
||||||
|
func (collection *PublishedRepoCollection) Update(repo *PublishedRepo) error {
|
||||||
|
err := collection.db.Put(repo.Key(), repo.Encode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadComplete loads additional information for remote repo
|
||||||
|
func (collection *PublishedRepoCollection) LoadComplete(repo *PublishedRepo, snapshotCollection *SnapshotCollection) error {
|
||||||
|
snapshot, err := snapshotCollection.ByUUID(repo.SnapshotUUID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.snapshot = snapshot
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByPrefixDistribution looks up repository by prefix & distribution
|
||||||
|
func (collection *PublishedRepoCollection) ByPrefixDistribution(prefix, distribution string) (*PublishedRepo, error) {
|
||||||
|
for _, r := range collection.list {
|
||||||
|
if r.Prefix == prefix && r.Distribution == distribution {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("published repo with prefix/distribution %s/%s not found", prefix, distribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByUUID looks up repository by uuid
|
||||||
|
func (collection *PublishedRepoCollection) ByUUID(uuid string) (*PublishedRepo, error) {
|
||||||
|
for _, r := range collection.list {
|
||||||
|
if r.UUID == uuid {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("published repo with uuid %s not found", uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForEach runs method for each repository
|
||||||
|
func (collection *PublishedRepoCollection) ForEach(handler func(*PublishedRepo) 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 *PublishedRepoCollection) Len() int {
|
||||||
|
return len(collection.list)
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+137
-2
@@ -1,6 +1,7 @@
|
|||||||
package debian
|
package debian
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/smira/aptly/database"
|
"github.com/smira/aptly/database"
|
||||||
. "launchpad.net/gocheck"
|
. "launchpad.net/gocheck"
|
||||||
"os"
|
"os"
|
||||||
@@ -25,6 +26,7 @@ type PublishedRepoSuite struct {
|
|||||||
PackageListMixinSuite
|
PackageListMixinSuite
|
||||||
repo *PublishedRepo
|
repo *PublishedRepo
|
||||||
packageRepo *Repository
|
packageRepo *Repository
|
||||||
|
snapshot *Snapshot
|
||||||
db database.Storage
|
db database.Storage
|
||||||
packageCollection *PackageCollection
|
packageCollection *PackageCollection
|
||||||
}
|
}
|
||||||
@@ -41,9 +43,9 @@ func (s *PublishedRepoSuite) SetUpTest(c *C) {
|
|||||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{})
|
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{})
|
||||||
repo.packageRefs = s.reflist
|
repo.packageRefs = s.reflist
|
||||||
|
|
||||||
snapshot, _ := NewSnapshotFromRepository("snap", repo)
|
s.snapshot, _ = NewSnapshotFromRepository("snap", repo)
|
||||||
|
|
||||||
s.repo = NewPublishedRepo("ppa", "squeeze", "main", nil, snapshot)
|
s.repo = NewPublishedRepo("ppa", "squeeze", "main", nil, s.snapshot)
|
||||||
|
|
||||||
s.packageCollection = NewPackageCollection(s.db)
|
s.packageCollection = NewPackageCollection(s.db)
|
||||||
s.packageCollection.Update(s.p1)
|
s.packageCollection.Update(s.p1)
|
||||||
@@ -57,6 +59,10 @@ func (s *PublishedRepoSuite) SetUpTest(c *C) {
|
|||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PublishedRepoSuite) TearDownTest(c *C) {
|
||||||
|
s.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *PublishedRepoSuite) TestPublish(c *C) {
|
func (s *PublishedRepoSuite) TestPublish(c *C) {
|
||||||
err := s.repo.Publish(s.packageRepo, s.packageCollection, &NullSigner{})
|
err := s.repo.Publish(s.packageRepo, s.packageCollection, &NullSigner{})
|
||||||
c.Assert(err, IsNil)
|
c.Assert(err, IsNil)
|
||||||
@@ -93,3 +99,132 @@ func (s *PublishedRepoSuite) TestPublish(c *C) {
|
|||||||
_, err = os.Stat(filepath.Join(s.packageRepo.RootPath, "public/ppa/pool/main/a/alien-arena/alien-arena-common_7.40-2_i386.deb"))
|
_, err = os.Stat(filepath.Join(s.packageRepo.RootPath, "public/ppa/pool/main/a/alien-arena/alien-arena-common_7.40-2_i386.deb"))
|
||||||
c.Assert(err, IsNil)
|
c.Assert(err, IsNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PublishedRepoSuite) TestString(c *C) {
|
||||||
|
c.Check(s.repo.String(), Equals,
|
||||||
|
"ppa/squeeze (main) publishes [snap]: Snapshot from mirror [yandex]: http://mirror.yandex.ru/debian/ squeeze")
|
||||||
|
c.Check(NewPublishedRepo("", "squeeze", "main", nil, s.snapshot).String(), Equals,
|
||||||
|
"./squeeze (main) publishes [snap]: Snapshot from mirror [yandex]: http://mirror.yandex.ru/debian/ squeeze")
|
||||||
|
c.Check(NewPublishedRepo("", "squeeze", "main", []string{"i386", "amd64"}, s.snapshot).String(), Equals,
|
||||||
|
"./squeeze (main) [i386, amd64] publishes [snap]: Snapshot from mirror [yandex]: http://mirror.yandex.ru/debian/ squeeze")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PublishedRepoSuite) TestKey(c *C) {
|
||||||
|
c.Check(s.repo.Key(), DeepEquals, []byte("Uppa>>squeeze"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PublishedRepoSuite) TestEncodeDecode(c *C) {
|
||||||
|
encoded := s.repo.Encode()
|
||||||
|
repo := &PublishedRepo{}
|
||||||
|
err := repo.Decode(encoded)
|
||||||
|
|
||||||
|
s.repo.snapshot = nil
|
||||||
|
c.Assert(err, IsNil)
|
||||||
|
c.Assert(repo, DeepEquals, s.repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublishedRepoCollectionSuite struct {
|
||||||
|
PackageListMixinSuite
|
||||||
|
db database.Storage
|
||||||
|
snapshotCollection *SnapshotCollection
|
||||||
|
collection *PublishedRepoCollection
|
||||||
|
snap1, snap2 *Snapshot
|
||||||
|
repo1, repo2, repo3 *PublishedRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = Suite(&PublishedRepoCollectionSuite{})
|
||||||
|
|
||||||
|
func (s *PublishedRepoCollectionSuite) SetUpTest(c *C) {
|
||||||
|
s.db, _ = database.OpenDB(c.MkDir())
|
||||||
|
|
||||||
|
s.snapshotCollection = NewSnapshotCollection(s.db)
|
||||||
|
|
||||||
|
s.snap1 = NewSnapshotFromPackageList("snap1", []*Snapshot{}, NewPackageList(), "desc1")
|
||||||
|
s.snap2 = NewSnapshotFromPackageList("snap2", []*Snapshot{}, NewPackageList(), "desc2")
|
||||||
|
|
||||||
|
s.snapshotCollection.Add(s.snap1)
|
||||||
|
s.snapshotCollection.Add(s.snap2)
|
||||||
|
|
||||||
|
s.repo1 = NewPublishedRepo("ppa", "anaconda", "main", []string{}, s.snap1)
|
||||||
|
s.repo2 = NewPublishedRepo("", "anaconda", "main", []string{}, s.snap2)
|
||||||
|
s.repo3 = NewPublishedRepo("ppa", "anaconda", "main", []string{}, s.snap2)
|
||||||
|
|
||||||
|
s.collection = NewPublishedRepoCollection(s.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PublishedRepoCollectionSuite) TearDownTest(c *C) {
|
||||||
|
s.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PublishedRepoCollectionSuite) TestAddByName(c *C) {
|
||||||
|
r, err := s.collection.ByPrefixDistribution("ppa", "anaconda")
|
||||||
|
c.Assert(err, ErrorMatches, "*.not found")
|
||||||
|
|
||||||
|
c.Assert(s.collection.Add(s.repo1), IsNil)
|
||||||
|
c.Assert(s.collection.Add(s.repo1), ErrorMatches, ".*already exists")
|
||||||
|
c.Assert(s.collection.CheckDuplicate(s.repo2), IsNil)
|
||||||
|
c.Assert(s.collection.Add(s.repo2), IsNil)
|
||||||
|
c.Assert(s.collection.Add(s.repo3), ErrorMatches, ".*already exists")
|
||||||
|
c.Assert(s.collection.CheckDuplicate(s.repo3), Equals, s.repo1)
|
||||||
|
|
||||||
|
r, err = s.collection.ByPrefixDistribution("ppa", "anaconda")
|
||||||
|
c.Assert(err, IsNil)
|
||||||
|
|
||||||
|
err = s.collection.LoadComplete(r, s.snapshotCollection)
|
||||||
|
c.Assert(err, IsNil)
|
||||||
|
c.Assert(r.String(), Equals, s.repo1.String())
|
||||||
|
|
||||||
|
collection := NewPublishedRepoCollection(s.db)
|
||||||
|
r, err = collection.ByPrefixDistribution("ppa", "anaconda")
|
||||||
|
c.Assert(err, IsNil)
|
||||||
|
|
||||||
|
err = s.collection.LoadComplete(r, s.snapshotCollection)
|
||||||
|
c.Assert(err, IsNil)
|
||||||
|
c.Assert(r.String(), Equals, s.repo1.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PublishedRepoCollectionSuite) TestByUUID(c *C) {
|
||||||
|
r, err := s.collection.ByUUID(s.repo1.UUID)
|
||||||
|
c.Assert(err, ErrorMatches, "*.not found")
|
||||||
|
|
||||||
|
c.Assert(s.collection.Add(s.repo1), IsNil)
|
||||||
|
|
||||||
|
r, err = s.collection.ByUUID(s.repo1.UUID)
|
||||||
|
c.Assert(err, IsNil)
|
||||||
|
|
||||||
|
err = s.collection.LoadComplete(r, s.snapshotCollection)
|
||||||
|
c.Assert(err, IsNil)
|
||||||
|
c.Assert(r.String(), Equals, s.repo1.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PublishedRepoCollectionSuite) TestUpdateLoadComplete(c *C) {
|
||||||
|
c.Assert(s.collection.Update(s.repo1), IsNil)
|
||||||
|
|
||||||
|
collection := NewPublishedRepoCollection(s.db)
|
||||||
|
r, err := collection.ByPrefixDistribution("ppa", "anaconda")
|
||||||
|
c.Assert(err, IsNil)
|
||||||
|
c.Assert(r.snapshot, IsNil)
|
||||||
|
c.Assert(s.collection.LoadComplete(r, s.snapshotCollection), IsNil)
|
||||||
|
c.Assert(r.snapshot.UUID, Equals, s.repo1.snapshot.UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PublishedRepoCollectionSuite) TestForEachAndLen(c *C) {
|
||||||
|
s.collection.Add(s.repo1)
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
err := s.collection.ForEach(func(*PublishedRepo) error {
|
||||||
|
count++
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
c.Assert(count, Equals, 1)
|
||||||
|
c.Assert(err, IsNil)
|
||||||
|
|
||||||
|
c.Check(s.collection.Len(), Equals, 1)
|
||||||
|
|
||||||
|
e := errors.New("c")
|
||||||
|
|
||||||
|
err = s.collection.ForEach(func(*PublishedRepo) error {
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
c.Assert(err, Equals, e)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user