diff --git a/daemons/atorrent/.gqlgen.yml b/daemons/atorrent/.gqlgen.yml deleted file mode 100644 index 0af5731..0000000 --- a/daemons/atorrent/.gqlgen.yml +++ /dev/null @@ -1,64 +0,0 @@ -schema: - - graphql/*.graphql - - graphql/**/*.graphql - - ../../graphql/types/*.graphql - -exec: - filename: delivery/graphql/generated.go -model: - filename: delivery/graphql/models_gen.go -resolver: - type: Resolver - layout: follow-schema - dir: delivery/graphql - -autobind: - - "git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model" - -models: - DateTime: - model: github.com/99designs/gqlgen/graphql.Time - Int: - model: github.com/99designs/gqlgen/graphql.Int64 - UInt: - model: - - github.com/99designs/gqlgen/graphql.Uint - Torrent: - extraFields: - T: - type: "*git.kmsign.ru/royalcat/tstor/daemons/atorrent.Controller" - TorrentFile: - extraFields: - F: - type: "*git.kmsign.ru/royalcat/tstor/daemons/atorrent.FileController" - TorrentPeer: - extraFields: - F: - type: "*github.com/anacrolix/torrent.PeerConn" - SimpleDir: - extraFields: - Path: - type: string - FS: - type: "git.kmsign.ru/royalcat/tstor/src/vfs.Filesystem" - TorrentFS: - extraFields: - FS: - type: "*git.kmsign.ru/royalcat/tstor/daemons/atorrent.TorrentFS" - TorrentOps: - extraFields: - InfoHash: - type: "string" - TorrentPriority: - model: "github.com/anacrolix/torrent/types.PiecePriority" - enum_values: - NONE: - value: "github.com/anacrolix/torrent/types.PiecePriorityNone" - NORMAL: - value: "github.com/anacrolix/torrent/types.PiecePriorityNormal" - HIGH: - value: "github.com/anacrolix/torrent/types.PiecePriorityHigh" - READAHEAD: - value: "github.com/anacrolix/torrent/types.PiecePriorityReadahead" - NOW: - value: "github.com/anacrolix/torrent/types.PiecePriorityNow" diff --git a/daemons/atorrent/client.go b/daemons/atorrent/client.go deleted file mode 100644 index e0e6b98..0000000 --- a/daemons/atorrent/client.go +++ /dev/null @@ -1,110 +0,0 @@ -package atorrent - -import ( - "crypto/rand" - "log/slog" - "os" - - "git.kmsign.ru/royalcat/tstor/src/config" - "git.kmsign.ru/royalcat/tstor/src/logwrap" - "github.com/anacrolix/dht/v2/bep44" - tlog "github.com/anacrolix/log" - "github.com/anacrolix/torrent" - "github.com/anacrolix/torrent/storage" - "github.com/anacrolix/torrent/types/infohash" -) - -func newClientConfig(st storage.ClientImpl, fis bep44.Store, cfg *config.TorrentClient, id [20]byte) *torrent.ClientConfig { - l := slog.With("component", "torrent-client") - - // TODO download and upload limits - torrentCfg := torrent.NewDefaultClientConfig() - torrentCfg.PeerID = string(id[:]) - torrentCfg.DefaultStorage = st - // torrentCfg.AlwaysWantConns = true - torrentCfg.DropMutuallyCompletePeers = true - // torrentCfg.TorrentPeersLowWater = 100 - // torrentCfg.TorrentPeersHighWater = 1000 - // torrentCfg.AcceptPeerConnections = true - torrentCfg.Seed = true - torrentCfg.DisableAggressiveUpload = false - - torrentCfg.PeriodicallyAnnounceTorrentsToDht = true - // torrentCfg.ConfigureAnacrolixDhtServer = func(cfg *dht.ServerConfig) { - // cfg.Store = fis - // cfg.Exp = dhtTTL - // cfg.PeerStore = fis - // } - - tl := tlog.NewLogger("torrent-client") - tl.SetHandlers(&logwrap.Torrent{L: l}) - - torrentCfg.Logger = tl - torrentCfg.Callbacks.NewPeer = append(torrentCfg.Callbacks.NewPeer, func(p *torrent.Peer) { - l.With(peerAttrs(p)...).Debug("new peer") - }) - torrentCfg.Callbacks.PeerClosed = append(torrentCfg.Callbacks.PeerClosed, func(p *torrent.Peer) { - l.With(peerAttrs(p)...).Debug("peer closed") - }) - torrentCfg.Callbacks.CompletedHandshake = func(pc *torrent.PeerConn, ih infohash.T) { - attrs := append(peerAttrs(&pc.Peer), slog.String("infohash", ih.HexString())) - l.With(attrs...).Debug("completed handshake") - } - torrentCfg.Callbacks.PeerConnAdded = append(torrentCfg.Callbacks.PeerConnAdded, func(pc *torrent.PeerConn) { - l.With(peerAttrs(&pc.Peer)...).Debug("peer conn added") - }) - torrentCfg.Callbacks.PeerConnClosed = func(pc *torrent.PeerConn) { - l.With(peerAttrs(&pc.Peer)...).Debug("peer conn closed") - } - torrentCfg.Callbacks.CompletedHandshake = func(pc *torrent.PeerConn, ih infohash.T) { - attrs := append(peerAttrs(&pc.Peer), slog.String("infohash", ih.HexString())) - l.With(attrs...).Debug("completed handshake") - } - torrentCfg.Callbacks.ReceivedRequested = append(torrentCfg.Callbacks.ReceivedRequested, func(pme torrent.PeerMessageEvent) { - l.With(peerAttrs(pme.Peer)...).Debug("received requested") - }) - torrentCfg.Callbacks.ReceivedUsefulData = append(torrentCfg.Callbacks.ReceivedUsefulData, func(pme torrent.PeerMessageEvent) { - l.With(peerAttrs(pme.Peer)...).Debug("received useful data") - }) - - return torrentCfg -} - -var emptyBytes [20]byte - -func getOrCreatePeerID(p string) ([20]byte, error) { - idb, err := os.ReadFile(p) - if err == nil { - var out [20]byte - copy(out[:], idb) - - return out, nil - } - - if !os.IsNotExist(err) { - return emptyBytes, err - } - - var out [20]byte - _, err = rand.Read(out[:]) - if err != nil { - return emptyBytes, err - } - - return out, os.WriteFile(p, out[:], 0755) -} - -func peerAttrs(peer *torrent.Peer) []any { - out := []any{ - slog.String("ip", peer.RemoteAddr.String()), - slog.String("discovery", string(peer.Discovery)), - slog.Int("max-requests", peer.PeerMaxRequests), - slog.Bool("prefers-encryption", peer.PeerPrefersEncryption), - } - - if peer.Torrent() != nil { - out = append(out, slog.String("torrent", peer.Torrent().Name())) - } - - return out -} diff --git a/daemons/atorrent/controller.go b/daemons/atorrent/controller.go deleted file mode 100644 index 42a96f6..0000000 --- a/daemons/atorrent/controller.go +++ /dev/null @@ -1,265 +0,0 @@ -package atorrent - -import ( - "context" - "log/slog" - "strings" - - "git.kmsign.ru/royalcat/tstor/pkg/kvsingle" - "git.kmsign.ru/royalcat/tstor/pkg/rlog" - "github.com/anacrolix/torrent" - "github.com/anacrolix/torrent/types" - "github.com/royalcat/kv" -) - -type TorrentFileDeleter interface { - DeleteFile(file *torrent.File) error -} - -type FileProperties struct { - Excluded bool `json:"excluded"` - Priority types.PiecePriority `json:"priority"` -} - -type Controller struct { - torrentFilePath string - t *torrent.Torrent - storage TorrentFileDeleter - fileProperties kv.Store[string, FileProperties] - log *rlog.Logger -} - -func newController(t *torrent.Torrent, torrentFileProperties kv.Store[string, FileProperties], storage TorrentFileDeleter, log *rlog.Logger) *Controller { - return &Controller{ - t: t, - storage: storage, - fileProperties: torrentFileProperties, - log: log.WithComponent("controller").With(slog.String("infohash", t.InfoHash().HexString())), - } -} - -func (s *Controller) TorrentFilePath() string { - return s.torrentFilePath -} - -func (s *Controller) Torrent() *torrent.Torrent { - return s.t -} - -func (c *Controller) Name() string { - <-c.t.GotInfo() - if name := c.t.Name(); name != "" { - return name - } - - return c.InfoHash() -} - -func (s *Controller) InfoHash() string { - <-s.t.GotInfo() - return s.t.InfoHash().HexString() -} - -func (s *Controller) BytesCompleted() int64 { - <-s.t.GotInfo() - return s.t.BytesCompleted() -} - -func (s *Controller) BytesMissing() int64 { - <-s.t.GotInfo() - return s.t.BytesMissing() -} - -func (s *Controller) Length() int64 { - <-s.t.GotInfo() - return s.t.Length() -} - -func (s *Controller) Files(ctx context.Context) ([]*FileController, error) { - ctx, span := tracer.Start(ctx, "Files") - defer span.End() - - fps := map[string]FileProperties{} - err := s.fileProperties.Range(ctx, func(k string, v FileProperties) error { - fps[k] = v - return nil - }) - if err != nil { - return nil, err - } - - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-s.t.GotInfo(): - } - - files := make([]*FileController, 0) - for _, v := range s.t.Files() { - if strings.Contains(v.Path(), "/.pad/") { - continue - } - - props := kvsingle.New(s.fileProperties, v.Path()) - ctl := NewFileController(v, props, s.log) - files = append(files, ctl) - } - - return files, nil -} - -func (s *Controller) GetFile(ctx context.Context, file string) (*FileController, error) { - files, err := s.Files(ctx) - if err != nil { - return nil, err - } - - for _, v := range files { - if v.Path() == file { - return v, nil - } - } - - return nil, nil -} - -func Map[T, U any](ts []T, f func(T) U) []U { - us := make([]U, len(ts)) - for i := range ts { - us[i] = f(ts[i]) - } - return us -} - -func (s *Controller) ExcludeFile(ctx context.Context, f *torrent.File) error { - log := s.log.With(slog.String("file", f.Path())) - log.Info(ctx, "excluding file") - - err := s.fileProperties.Edit(ctx, f.Path(), func(ctx context.Context, v FileProperties) (FileProperties, error) { - v.Excluded = true - return v, nil - }) - if err == kv.ErrKeyNotFound { - err := s.fileProperties.Set(ctx, f.Path(), FileProperties{Excluded: true}) - if err != nil { - return err - } - } else if err != nil { - return err - } - - return s.storage.DeleteFile(f) -} - -func (s *Controller) isFileComplete(startIndex int, endIndex int) bool { - for i := startIndex; i < endIndex; i++ { - if !s.t.Piece(i).State().Complete { - return false - } - } - return true -} - -func (s *Controller) ValidateTorrent(ctx context.Context) error { - select { - case <-ctx.Done(): - return ctx.Err() - case <-s.t.GotInfo(): - } - - for i := 0; i < s.t.NumPieces(); i++ { - if ctx.Err() != nil { - return ctx.Err() - } - - s.t.Piece(i).VerifyData() - } - - return nil -} - -func (c *Controller) SetPriority(ctx context.Context, priority types.PiecePriority) error { - log := c.log.With(slog.Int("priority", int(priority))) - files, err := c.Files(ctx) - if err != nil { - return err - } - for _, f := range files { - excluded, err := f.Excluded(ctx) - if err != nil { - log.Error(ctx, "failed to get file exclusion status", rlog.Error(err)) - } - if excluded { - continue - } - - err = f.SetPriority(ctx, priority) - if err != nil { - log.Error(ctx, "failed to set file priority", rlog.Error(err)) - } - } - return nil -} - -const defaultPriority = types.PiecePriorityNone - -func (c *Controller) Priority(ctx context.Context) (types.PiecePriority, error) { - prio := defaultPriority - files, err := c.Files(ctx) - if err != nil { - return 0, err - } - for _, v := range files { - filePriority := v.Priority() - if filePriority > prio { - prio = filePriority - } - } - - return prio, nil -} - -// func (c *Controller) setFilePriority(ctx context.Context, file *torrent.File, priority types.PiecePriority) error { -// err := c.fileProperties.Edit(ctx, file.Path(), func(ctx context.Context, v FileProperties) (FileProperties, error) { -// v.Priority = priority -// return v, nil -// }) - -// if err == kv.ErrKeyNotFound { -// seterr := c.fileProperties.Set(ctx, file.Path(), FileProperties{Priority: priority}) -// if seterr != nil { -// return seterr -// } -// err = nil -// } - -// if err != nil { -// return err -// } -// file.SetPriority(priority) -// return nil -// } - -func (c *Controller) initializeTorrentPriories(ctx context.Context) error { - ctx, span := tracer.Start(ctx, "initializeTorrentPriories") - defer span.End() - log := c.log - - files, err := c.Files(ctx) - if err != nil { - return err - } - - for _, file := range files { - props, err := file.Properties(ctx) - if err != nil { - log.Error(ctx, "failed to get file properties", rlog.Error(err)) - continue - } - file.file.SetPriority(props.Priority) - } - - log.Debug(ctx, "torrent initialization complete", slog.String("infohash", c.InfoHash()), slog.String("torrent_name", c.Name())) - - return nil -} diff --git a/daemons/atorrent/daemon.go b/daemons/atorrent/daemon.go deleted file mode 100644 index 97aa766..0000000 --- a/daemons/atorrent/daemon.go +++ /dev/null @@ -1,226 +0,0 @@ -package atorrent - -import ( - "context" - "errors" - "fmt" - "os" - "path/filepath" - "sync" - "time" - - "git.kmsign.ru/royalcat/tstor/pkg/rlog" - "git.kmsign.ru/royalcat/tstor/src/config" - "git.kmsign.ru/royalcat/tstor/src/tkv" - "git.kmsign.ru/royalcat/tstor/src/vfs" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/trace" - "golang.org/x/exp/maps" - - "github.com/anacrolix/torrent" - "github.com/anacrolix/torrent/bencode" - "github.com/anacrolix/torrent/metainfo" - "github.com/anacrolix/torrent/types/infohash" - "github.com/go-git/go-billy/v5" - "github.com/royalcat/kv" -) - -const instrument = "git.kmsign.ru/royalcat/tstor/daemons/atorrent" - -var ( - tracer = otel.Tracer(instrument, trace.WithInstrumentationAttributes(attribute.String("component", "torrent-daemon"))) - meter = otel.Meter(instrument, metric.WithInstrumentationAttributes(attribute.String("component", "torrent-daemon"))) -) - -type DirAquire struct { - Name string - Hashes []infohash.T -} - -type Daemon struct { - client *torrent.Client - infoBytes *infoBytesStore - Storage *fileStorage - fis *dhtFileItemStore - dirsAquire kv.Store[string, DirAquire] - fileProperties kv.Store[string, FileProperties] - statsStore *statsStore - - loadMutex sync.Mutex - - sourceFs billy.Filesystem - - log *rlog.Logger -} - -const dhtTTL = 180 * 24 * time.Hour - -func NewDaemon(sourceFs billy.Filesystem, conf config.TorrentClient) (*Daemon, error) { - s := &Daemon{ - log: rlog.Component("torrent-service"), - sourceFs: sourceFs, - loadMutex: sync.Mutex{}, - } - - err := os.MkdirAll(conf.MetadataFolder, 0744) - if err != nil { - return nil, fmt.Errorf("error creating metadata folder: %w", err) - } - - s.fis, err = newDHTStore(filepath.Join(conf.MetadataFolder, "dht-item-store"), dhtTTL) - if err != nil { - return nil, fmt.Errorf("error starting item store: %w", err) - } - - s.Storage, _, err = setupStorage(conf) - if err != nil { - return nil, err - } - - s.fileProperties, err = tkv.NewKV[string, FileProperties](conf.MetadataFolder, "file-properties") - if err != nil { - return nil, err - } - - s.infoBytes, err = newInfoBytesStore(conf.MetadataFolder) - if err != nil { - return nil, err - } - - id, err := getOrCreatePeerID(filepath.Join(conf.MetadataFolder, "ID")) - if err != nil { - return nil, fmt.Errorf("error creating node ID: %w", err) - } - - s.statsStore, err = newStatsStore(conf.MetadataFolder, time.Hour*24*30) - if err != nil { - return nil, err - } - - clientConfig := newClientConfig(s.Storage, s.fis, &conf, id) - s.client, err = torrent.NewClient(clientConfig) - if err != nil { - return nil, err - } - - // TODO move to config - s.client.AddDhtNodes([]string{ - "router.bittorrent.com:6881", - "router.utorrent.com:6881", - "dht.transmissionbt.com:6881", - "router.bitcomet.com:6881", - "dht.aelitis.com6881", - }) - - s.client.AddDhtNodes(conf.DHTNodes) - - s.dirsAquire, err = tkv.NewKV[string, DirAquire](conf.MetadataFolder, "dir-acquire") - if err != nil { - return nil, err - } - - // go func() { - // ctx := context.Background() - // err := s.backgroudFileLoad(ctx) - // if err != nil { - // s.log.Error(ctx, "initial torrent load failed", rlog.Error(err)) - // } - // }() - - go func() { - ctx := context.Background() - const period = time.Second * 10 - - err := registerTorrentMetrics(s.client) - if err != nil { - s.log.Error(ctx, "error registering torrent metrics", rlog.Error(err)) - } - err = registerDhtMetrics(s.client) - if err != nil { - s.log.Error(ctx, "error registering dht metrics", rlog.Error(err)) - } - - timer := time.NewTicker(period) - for { - select { - case <-s.client.Closed(): - return - case <-timer.C: - s.updateStats(ctx) - } - } - }() - - return s, nil -} - -var _ vfs.FsFactory = (*Daemon)(nil).NewTorrentFs - -func (s *Daemon) Close(ctx context.Context) error { - return errors.Join(append( - s.client.Close(), - s.Storage.Close(), - s.dirsAquire.Close(ctx), - // s.excludedFiles.Close(ctx), - s.infoBytes.Close(), - s.fis.Close(), - )...) -} - -func isValidInfoHashBytes(d []byte) bool { - var info metainfo.Info - err := bencode.Unmarshal(d, &info) - return err == nil -} - -func (s *Daemon) Stats() torrent.ConnStats { - return s.client.Stats().ConnStats -} - -func storeByTorrent[K kv.Bytes, V any](s kv.Store[K, V], infohash infohash.T) kv.Store[K, V] { - return kv.PrefixBytes[K, V](s, K(infohash.HexString()+"/")) -} - -func (s *Daemon) newController(t *torrent.Torrent) *Controller { - return newController(t, - storeByTorrent(s.fileProperties, t.InfoHash()), - s.Storage, - s.log, - ) -} - -func (s *Daemon) ListTorrents(ctx context.Context) ([]*Controller, error) { - out := []*Controller{} - for _, v := range s.client.Torrents() { - out = append(out, s.newController(v)) - } - return out, nil -} - -func (s *Daemon) GetTorrent(infohashHex string) (*Controller, error) { - t, ok := s.client.Torrent(infohash.FromHexString(infohashHex)) - if !ok { - return nil, nil - } - - return s.newController(t), nil -} - -func slicesUnique[S ~[]E, E comparable](in S) S { - m := map[E]struct{}{} - for _, v := range in { - m[v] = struct{}{} - } - - return maps.Keys(m) -} - -func apply[I, O any](in []I, f func(e I) O) []O { - out := []O{} - for _, v := range in { - out = append(out, f(v)) - } - return out -} diff --git a/daemons/atorrent/daemon_load.go b/daemons/atorrent/daemon_load.go deleted file mode 100644 index 915ed64..0000000 --- a/daemons/atorrent/daemon_load.go +++ /dev/null @@ -1,246 +0,0 @@ -package atorrent - -import ( - "bufio" - "context" - "fmt" - "io" - "log/slog" - "os" - "strings" - "sync" - "time" - - "git.kmsign.ru/royalcat/tstor/pkg/ctxbilly" - "git.kmsign.ru/royalcat/tstor/pkg/rlog" - "git.kmsign.ru/royalcat/tstor/src/vfs" - "github.com/anacrolix/torrent" - "github.com/anacrolix/torrent/metainfo" - "github.com/go-git/go-billy/v5/util" - "github.com/royalcat/ctxio" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -const activityTimeout = time.Minute * 15 - -func readInfoHash(ctx context.Context, f vfs.File) (metainfo.Hash, error) { - ctx, span := tracer.Start(ctx, "readInfoHash") - defer span.End() - - mi, err := metainfo.Load(ctxio.IoReader(ctx, f)) - if err != nil { - return metainfo.Hash{}, fmt.Errorf("loading metainfo: %w", err) - } - - return mi.HashInfoBytes(), nil -} - -func (s *Daemon) loadTorrent(ctx context.Context, f vfs.File) (*Controller, error) { - ctx, span := tracer.Start(ctx, "loadTorrent") - defer span.End() - log := s.log - - stat, err := f.Info() - if err != nil { - return nil, fmt.Errorf("call stat failed: %w", err) - } - - span.SetAttributes(attribute.String("filename", stat.Name())) - - mi, err := metainfo.Load(bufio.NewReader(ctxio.IoReader(ctx, f))) - if err != nil { - return nil, fmt.Errorf("loading torrent metadata from file %s, error: %w", stat.Name(), err) - } - log = log.With(slog.String("info-hash", mi.HashInfoBytes().HexString())) - - var ctl *Controller - t, ok := s.client.Torrent(mi.HashInfoBytes()) - if ok { - log = log.With(slog.String("torrent-name", t.Name())) - ctl = s.newController(t) - } else { - span.AddEvent("torrent not found, loading from file") - log.Info(ctx, "torrent not found, loading from file") - - spec, err := torrent.TorrentSpecFromMetaInfoErr(mi) - if err != nil { - return nil, fmt.Errorf("parse spec from metadata: %w", err) - } - infoBytes := spec.InfoBytes - - if !isValidInfoHashBytes(infoBytes) { - log.Warn(ctx, "info loaded from spec not valid") - infoBytes = nil - } - - if len(infoBytes) == 0 { - log.Info(ctx, "no info loaded from file, try to load from cache") - infoBytes, err = s.infoBytes.GetBytes(spec.InfoHash) - if err != nil && err != errNotFound { - return nil, fmt.Errorf("get info bytes from database: %w", err) - } - } - - t, _ = s.client.AddTorrentOpt(torrent.AddTorrentOpts{ - InfoHash: spec.InfoHash, - InfoHashV2: spec.InfoHashV2, - Storage: s.Storage, - InfoBytes: infoBytes, - ChunkSize: spec.ChunkSize, - }) - - log = log.With(slog.String("torrent-name", t.Name())) - - t.AllowDataDownload() - t.AllowDataUpload() - - span.AddEvent("torrent added to client") - - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-t.GotInfo(): - err := s.infoBytes.Set(t.InfoHash(), t.Metainfo()) - if err != nil { - log.Error(ctx, "error setting info bytes for torrent", - slog.String("torrent-name", t.Name()), - rlog.Error(err), - ) - } - } - span.AddEvent("got info") - - ctl = s.newController(t) - - err = ctl.initializeTorrentPriories(ctx) - if err != nil { - return nil, fmt.Errorf("initialize torrent priorities: %w", err) - } - - // go func() { - // subscr := ctl.t.SubscribePieceStateChanges() - // defer subscr.Close() - // dropTimer := time.NewTimer(activityTimeout) - // defer dropTimer.Stop() - - // for { - // select { - // case <-subscr.Values: - // dropTimer.Reset(activityTimeout) - // case <-dropTimer.C: - // log.Info(ctx, "torrent dropped by activity timeout") - // select { - // case <-ctl.t.Closed(): - // return - // case <-time.After(time.Second): - // ctl.t.Drop() - // } - // case <-ctl.t.Closed(): - // return - // } - // } - // }() - } - - return ctl, nil -} - -const loadWorkers = 5 - -func (s *Daemon) backgroudFileLoad(ctx context.Context) error { - ctx, span := tracer.Start(ctx, "loadTorrentFiles", trace.WithAttributes( - attribute.Int("workers", loadWorkers), - )) - defer span.End() - log := s.log - - loaderPaths := make(chan string, loadWorkers*5) - wg := sync.WaitGroup{} - - defer func() { - close(loaderPaths) - wg.Wait() - }() - - loaderWorker := func() { - for path := range loaderPaths { - info, err := s.sourceFs.Stat(path) - if err != nil { - log.Error(ctx, "error stat torrent file", slog.String("filename", path), rlog.Error(err)) - continue - } - - file, err := s.sourceFs.Open(path) - if err != nil { - log.Error(ctx, "error opening torrent file", slog.String("filename", path), rlog.Error(err)) - continue - } - defer file.Close() - - vfile := vfs.NewCtxBillyFile(info, ctxbilly.WrapFile(file)) - - ih, err := readInfoHash(ctx, vfile) - if err != nil { - log.Error(ctx, "error reading info hash", slog.String("filename", path), rlog.Error(err)) - continue - } - props := storeByTorrent(s.fileProperties, ih) - _, err = vfile.Seek(0, io.SeekStart) - if err != nil { - log.Error(ctx, "error seeking file", slog.String("filename", path), rlog.Error(err)) - continue - } - - isPrioritized := false - err = props.Range(ctx, func(k string, v FileProperties) error { - if v.Priority > 0 { - isPrioritized = true - return io.EOF - } - return nil - }) - if err != nil && err != io.EOF { - log.Error(ctx, "error checking file priority", slog.String("filename", path), rlog.Error(err)) - continue - } - - if !isPrioritized { - log.Debug(ctx, "file not prioritized, skipping", slog.String("filename", path)) - continue - } - - _, err = s.loadTorrent(ctx, vfile) - if err != nil { - log.Error(ctx, "failed adding torrent", rlog.Error(err)) - } - } - - wg.Done() - } - - wg.Add(loadWorkers) - for range loadWorkers { - go loaderWorker() - } - - return util.Walk(s.sourceFs, "", func(path string, info os.FileInfo, err error) error { - if err != nil { - return fmt.Errorf("fs walk error: %w", err) - } - - if ctx.Err() != nil { - return ctx.Err() - } - - if info.IsDir() { - return nil - } - - if strings.HasSuffix(path, ".torrent") { - loaderPaths <- path - } - - return nil - }) -} diff --git a/daemons/atorrent/daemon_stats.go b/daemons/atorrent/daemon_stats.go deleted file mode 100644 index 3693b02..0000000 --- a/daemons/atorrent/daemon_stats.go +++ /dev/null @@ -1,73 +0,0 @@ -package atorrent - -import ( - "context" - "time" - - "git.kmsign.ru/royalcat/tstor/pkg/rlog" - "github.com/anacrolix/torrent/types/infohash" -) - -func (s *Daemon) allStats(ctx context.Context) (map[infohash.T]TorrentStats, TorrentStats) { - totalPeers := 0 - activePeers := 0 - connectedSeeders := 0 - - perTorrentStats := map[infohash.T]TorrentStats{} - - for _, v := range s.client.Torrents() { - stats := v.Stats() - perTorrentStats[v.InfoHash()] = TorrentStats{ - Timestamp: time.Now(), - DownloadedBytes: uint64(stats.BytesRead.Int64()), - UploadedBytes: uint64(stats.BytesWritten.Int64()), - TotalPeers: uint16(stats.TotalPeers), - ActivePeers: uint16(stats.ActivePeers), - ConnectedSeeders: uint16(stats.ConnectedSeeders), - } - - totalPeers += stats.TotalPeers - activePeers += stats.ActivePeers - connectedSeeders += stats.ConnectedSeeders - } - - totalStats := s.client.Stats() - - return perTorrentStats, TorrentStats{ - Timestamp: time.Now(), - DownloadedBytes: uint64(totalStats.BytesRead.Int64()), - UploadedBytes: uint64(totalStats.BytesWritten.Int64()), - TotalPeers: uint16(totalPeers), - ActivePeers: uint16(activePeers), - ConnectedSeeders: uint16(connectedSeeders), - } -} - -func (s *Daemon) updateStats(ctx context.Context) { - log := s.log - - perTorrentStats, totalStats := s.allStats(ctx) - for ih, v := range perTorrentStats { - err := s.statsStore.AddTorrentStats(ih, v) - if err != nil { - log.Error(ctx, "error saving torrent stats", rlog.Error(err)) - } - } - - err := s.statsStore.AddTotalStats(totalStats) - if err != nil { - log.Error(ctx, "error saving total stats", rlog.Error(err)) - } -} - -func (s *Daemon) TotalStatsHistory(ctx context.Context, since time.Time) ([]TorrentStats, error) { - return s.statsStore.ReadTotalStatsHistory(ctx, since) -} - -func (s *Daemon) TorrentStatsHistory(ctx context.Context, since time.Time, ih infohash.T) ([]TorrentStats, error) { - return s.statsStore.ReadTorrentStatsHistory(ctx, since, ih) -} - -func (s *Daemon) StatsHistory(ctx context.Context, since time.Time) ([]TorrentStats, error) { - return s.statsStore.ReadStatsHistory(ctx, since) -} diff --git a/daemons/atorrent/delivery/graphql/fs.resolvers.go b/daemons/atorrent/delivery/graphql/fs.resolvers.go deleted file mode 100644 index 99516d8..0000000 --- a/daemons/atorrent/delivery/graphql/fs.resolvers.go +++ /dev/null @@ -1,25 +0,0 @@ -package graphql - -import ( - "context" - - "git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model" -) - -func (r *torrentFSResolver) Entries(ctx context.Context, obj *model.TorrentFs) ([]model.FsEntry, error) { - // entries, err := obj.FS.ReadDir(ctx, ".") - // if err != nil { - // return nil, err - // } - out := []model.FsEntry{} - // for _, e := range entries { - // entry, err := model.FillFsEntry(ctx, e, obj.FS, ".") - // if err != nil { - // return nil, err - // } - // out = append(out, entry) - // } - return out, nil -} - -type torrentFSResolver struct{ *Resolver } diff --git a/daemons/atorrent/delivery/graphql/mappers.go b/daemons/atorrent/delivery/graphql/mappers.go deleted file mode 100644 index 00c2fcf..0000000 --- a/daemons/atorrent/delivery/graphql/mappers.go +++ /dev/null @@ -1,56 +0,0 @@ -package graphql - -import ( - "context" - - "git.kmsign.ru/royalcat/tstor/daemons/atorrent" - "github.com/anacrolix/torrent" -) - -func MapPeerSource(source torrent.PeerSource) string { - switch source { - case torrent.PeerSourceDirect: - return "Direct" - case torrent.PeerSourceUtHolepunch: - return "Ut Holepunch" - case torrent.PeerSourceDhtAnnouncePeer: - return "DHT Announce" - case torrent.PeerSourceDhtGetPeers: - return "DHT" - case torrent.PeerSourceIncoming: - return "Incoming" - case torrent.PeerSourceTracker: - return "Tracker" - case torrent.PeerSourcePex: - return "PEX" - default: - return "Unknown" - } -} - -func MapTorrent(ctx context.Context, t *atorrent.Controller) (*Torrent, error) { - prio, err := t.Priority(ctx) - if err != nil { - return nil, err - } - - return &Torrent{ - Infohash: t.InfoHash(), - Name: t.Name(), - BytesCompleted: t.BytesCompleted(), - BytesMissing: t.BytesMissing(), - Priority: prio, - T: t, - }, nil -} - -func MapTorrentStats(s atorrent.TorrentStats) *TorrentStats { - return &TorrentStats{ - Timestamp: s.Timestamp, - DownloadedBytes: uint(s.DownloadedBytes), - UploadedBytes: uint(s.UploadedBytes), - TotalPeers: uint(s.TotalPeers), - ActivePeers: uint(s.ActivePeers), - ConnectedSeeders: uint(s.ConnectedSeeders), - } -} diff --git a/daemons/atorrent/delivery/graphql/models_gen.go b/daemons/atorrent/delivery/graphql/models_gen.go deleted file mode 100644 index 856fa11..0000000 --- a/daemons/atorrent/delivery/graphql/models_gen.go +++ /dev/null @@ -1,140 +0,0 @@ -// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. - -package graphql - -import ( - "time" - - "git.kmsign.ru/royalcat/tstor/daemons/atorrent" - "git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model" - "github.com/anacrolix/torrent" - "github.com/anacrolix/torrent/types" -) - -type CleanupResponse struct { - Count int64 `json:"count"` - List []string `json:"list"` -} - -type DownloadTorrentResponse struct { - TaskID string `json:"taskID"` -} - -type Torrent struct { - Name string `json:"name"` - Infohash string `json:"infohash"` - BytesCompleted int64 `json:"bytesCompleted"` - TorrentFilePath string `json:"torrentFilePath"` - BytesMissing int64 `json:"bytesMissing"` - Priority types.PiecePriority `json:"priority"` - Files []*TorrentFile `json:"files"` - ExcludedFiles []*TorrentFile `json:"excludedFiles"` - Peers []*TorrentPeer `json:"peers"` - T *atorrent.Controller `json:"-"` -} - -type TorrentClientStats struct { - BytesWritten int64 `json:"bytesWritten"` - BytesWrittenData int64 `json:"bytesWrittenData"` - BytesRead int64 `json:"bytesRead"` - BytesReadData int64 `json:"bytesReadData"` - BytesReadUsefulData int64 `json:"bytesReadUsefulData"` - BytesReadUsefulIntendedData int64 `json:"bytesReadUsefulIntendedData"` - ChunksWritten int64 `json:"chunksWritten"` - ChunksRead int64 `json:"chunksRead"` - ChunksReadUseful int64 `json:"chunksReadUseful"` - ChunksReadWasted int64 `json:"chunksReadWasted"` - MetadataChunksRead int64 `json:"metadataChunksRead"` - PiecesDirtiedGood int64 `json:"piecesDirtiedGood"` - PiecesDirtiedBad int64 `json:"piecesDirtiedBad"` -} - -type TorrentDaemonMutation struct { - ValidateTorrent bool `json:"validateTorrent"` - SetTorrentPriority bool `json:"setTorrentPriority"` - Cleanup *CleanupResponse `json:"cleanup"` -} - -type TorrentDaemonQuery struct { - Torrents []*Torrent `json:"torrents"` - ClientStats *TorrentClientStats `json:"clientStats"` - StatsHistory []*TorrentStats `json:"statsHistory"` -} - -type TorrentFs struct { - Name string `json:"name"` - Torrent *Torrent `json:"torrent"` - Entries []model.FsEntry `json:"entries"` - FS *atorrent.TorrentFS `json:"-"` -} - -func (TorrentFs) IsDir() {} - -func (TorrentFs) IsFsEntry() {} - -type TorrentFile struct { - Filename string `json:"filename"` - Size int64 `json:"size"` - BytesCompleted int64 `json:"bytesCompleted"` - Priority types.PiecePriority `json:"priority"` - F *atorrent.FileController `json:"-"` -} - -type TorrentFileEntry struct { - Name string `json:"name"` - Torrent *Torrent `json:"torrent"` - Size int64 `json:"size"` -} - -func (TorrentFileEntry) IsFile() {} - -func (TorrentFileEntry) IsFsEntry() {} - -type TorrentFilter struct { - Everything *bool `json:"everything,omitempty"` - Infohash *string `json:"infohash,omitempty"` -} - -type TorrentPeer struct { - IP string `json:"ip"` - DownloadRate float64 `json:"downloadRate"` - Discovery string `json:"discovery"` - Port int64 `json:"port"` - ClientName string `json:"clientName"` - F *torrent.PeerConn `json:"-"` -} - -type TorrentPriorityFilter struct { - Eq *types.PiecePriority `json:"eq,omitempty"` - Gt *types.PiecePriority `json:"gt,omitempty"` - Lt *types.PiecePriority `json:"lt,omitempty"` - Gte *types.PiecePriority `json:"gte,omitempty"` - Lte *types.PiecePriority `json:"lte,omitempty"` - In []types.PiecePriority `json:"in,omitempty"` -} - -type TorrentProgress struct { - Torrent *Torrent `json:"torrent"` - Current int64 `json:"current"` - Total int64 `json:"total"` -} - -func (TorrentProgress) IsProgress() {} - -type TorrentStats struct { - Timestamp time.Time `json:"timestamp"` - DownloadedBytes uint `json:"downloadedBytes"` - UploadedBytes uint `json:"uploadedBytes"` - TotalPeers uint `json:"totalPeers"` - ActivePeers uint `json:"activePeers"` - ConnectedSeeders uint `json:"connectedSeeders"` -} - -type TorrentsFilter struct { - Infohash *model.StringFilter `json:"infohash,omitempty"` - Name *model.StringFilter `json:"name,omitempty"` - BytesCompleted *model.IntFilter `json:"bytesCompleted,omitempty"` - BytesMissing *model.IntFilter `json:"bytesMissing,omitempty"` - PeersCount *model.IntFilter `json:"peersCount,omitempty"` - Priority *TorrentPriorityFilter `json:"priority,omitempty"` -} diff --git a/daemons/atorrent/delivery/graphql/resolver.go b/daemons/atorrent/delivery/graphql/resolver.go deleted file mode 100644 index b6b4487..0000000 --- a/daemons/atorrent/delivery/graphql/resolver.go +++ /dev/null @@ -1,17 +0,0 @@ -package graphql - -import ( - "git.kmsign.ru/royalcat/tstor/daemons/atorrent" - "git.kmsign.ru/royalcat/tstor/src/vfs" - "github.com/go-git/go-billy/v5" -) - -// This file will not be regenerated automatically. -// -// It serves as dependency injection for your app, add any dependencies you require here. - -type Resolver struct { - ATorrentDaemon *atorrent.Daemon - VFS vfs.Filesystem - SourceFS billy.Filesystem -} diff --git a/daemons/atorrent/delivery/graphql/subscription.resolvers.go b/daemons/atorrent/delivery/graphql/subscription.resolvers.go deleted file mode 100644 index f1fc6b9..0000000 --- a/daemons/atorrent/delivery/graphql/subscription.resolvers.go +++ /dev/null @@ -1,47 +0,0 @@ -package graphql - -// This file will be automatically regenerated based on the schema, any resolver implementations -// will be copied through when generating and any unknown code will be moved to the end. -// Code generated by github.com/99designs/gqlgen version v0.17.55 - -import ( - "context" - "fmt" -) - -type subscriptionResolver struct{ *Resolver } - -func (r *subscriptionResolver) TorrentDownloadUpdates(ctx context.Context) (<-chan *TorrentProgress, error) { - out := make(chan *TorrentProgress) - progress, err := r.ATorrentDaemon.DownloadProgress(ctx) - if err != nil { - return nil, err - } - - go func() { - defer close(out) - for p := range progress { - if p.Torrent == nil { - fmt.Println("nil torrent") - continue - } - torrent, err := MapTorrent(ctx, p.Torrent) - if err != nil { - // TODO logs - continue - } - po := &TorrentProgress{ - Torrent: torrent, - Current: p.Current, - Total: p.Total, - } - select { - case <-ctx.Done(): - return - case out <- po: - } - } - }() - - return out, nil -} diff --git a/daemons/atorrent/delivery/graphql/torrent_mutation.resolvers.go b/daemons/atorrent/delivery/graphql/torrent_mutation.resolvers.go deleted file mode 100644 index f45fc86..0000000 --- a/daemons/atorrent/delivery/graphql/torrent_mutation.resolvers.go +++ /dev/null @@ -1,102 +0,0 @@ -package graphql - -// This file will be automatically regenerated based on the schema, any resolver implementations -// will be copied through when generating and any unknown code will be moved to the end. -// Code generated by github.com/99designs/gqlgen version v0.17.55 - -import ( - "context" - - graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql" - "git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model" - "github.com/anacrolix/torrent/types" -) - -// ValidateTorrent is the resolver for the validateTorrent field. -func (r *torrentDaemonMutationResolver) ValidateTorrent(ctx context.Context, obj *model.TorrentDaemonMutation, filter model.TorrentFilter) (bool, error) { - if filter.Infohash != nil { - t, err := r.Resolver.ATorrentDaemon.GetTorrent(*filter.Infohash) - if err != nil { - return false, err - } - if t == nil { - return false, nil - } - - t.ValidateTorrent(ctx) - return true, nil - } - - if filter.Everything != nil && *filter.Everything { - torrents, err := r.Resolver.ATorrentDaemon.ListTorrents(ctx) - if err != nil { - return false, err - } - for _, v := range torrents { - if err := v.ValidateTorrent(ctx); err != nil { - return false, err - } - } - return true, nil - } - - return false, nil -} - -// SetTorrentPriority is the resolver for the setTorrentPriority field. -func (r *torrentDaemonMutationResolver) SetTorrentPriority(ctx context.Context, obj *model.TorrentDaemonMutation, infohash string, file *string, priority types.PiecePriority) (bool, error) { - t, err := r.Resolver.ATorrentDaemon.GetTorrent(infohash) - if err != nil { - return false, err - } - if t == nil { - return false, nil - } - - if file == nil { - err = t.SetPriority(ctx, priority) - if err != nil { - return false, err - } - return true, nil - } - - f, err := t.GetFile(ctx, *file) - if err != nil { - return false, err - } - err = f.SetPriority(ctx, priority) - if err != nil { - return false, err - } - return true, nil -} - -// Cleanup is the resolver for the cleanup field. -func (r *torrentDaemonMutationResolver) Cleanup(ctx context.Context, obj *model.TorrentDaemonMutation, files *bool, dryRun bool) (*model.CleanupResponse, error) { - torrents, err := r.ATorrentDaemon.ListTorrents(ctx) - if err != nil { - return nil, err - } - - if files != nil && *files { - r, err := r.ATorrentDaemon.Storage.CleanupFiles(ctx, torrents, dryRun) - return &model.CleanupResponse{ - Count: int64(len(r)), - List: r, - }, err - } else { - r, err := r.ATorrentDaemon.Storage.CleanupDirs(ctx, torrents, dryRun) - return &model.CleanupResponse{ - Count: int64(len(r)), - List: r, - }, err - } -} - -// TorrentDaemonMutation returns graph.TorrentDaemonMutationResolver implementation. -func (r *Resolver) TorrentDaemonMutation() graph.TorrentDaemonMutationResolver { - return &torrentDaemonMutationResolver{r} -} - -type torrentDaemonMutationResolver struct{ *Resolver } diff --git a/daemons/atorrent/delivery/graphql/torrent_query.resolvers.go b/daemons/atorrent/delivery/graphql/torrent_query.resolvers.go deleted file mode 100644 index da4c2b6..0000000 --- a/daemons/atorrent/delivery/graphql/torrent_query.resolvers.go +++ /dev/null @@ -1,142 +0,0 @@ -package graphql - -// This file will be automatically regenerated based on the schema, any resolver implementations -// will be copied through when generating and any unknown code will be moved to the end. -// Code generated by github.com/99designs/gqlgen version v0.17.55 - -import ( - "context" - "time" - - graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql" - "git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model" -) - -// Torrents is the resolver for the torrents field. -func (r *torrentDaemonQueryResolver) Torrents(ctx context.Context, obj *model.TorrentDaemonQuery, filter *model.TorrentsFilter) ([]*model.Torrent, error) { - // torrents, err := r.ATorrentDaemon.ListTorrents(ctx) - // if err != nil { - // return nil, err - // } - - // filterFuncs := []func(torrent *model.Torrent) bool{} - - // if filter != nil { - // if filter.BytesCompleted != nil { - // filterFuncs = append(filterFuncs, func(torrent *model.Torrent) bool { - // return filter.BytesCompleted.Include(torrent.BytesCompleted) - // }) - // } - // if filter.BytesMissing != nil { - // filterFuncs = append(filterFuncs, func(torrent *model.Torrent) bool { - // return filter.BytesMissing.Include(torrent.BytesMissing) - // }) - // } - // if filter.PeersCount != nil { - // filterFuncs = append(filterFuncs, func(torrent *model.Torrent) bool { - // return filter.PeersCount.Include( - // int64(len(torrent.T.Torrent().PeerConns())), - // ) - // }) - // } - // if filter.Infohash != nil { - // filterFuncs = append(filterFuncs, func(torrent *model.Torrent) bool { - // return filter.Infohash.Include( - // torrent.Infohash, - // ) - // }) - // } - // if filter.Priority != nil { - // filterFuncs = append(filterFuncs, func(torrent *model.Torrent) bool { - // return filter.Priority.Include( - // torrent.Priority, - // ) - // }) - // } - // } - - // filterFunc := func(torrent *model.Torrent) bool { - // for _, f := range filterFuncs { - // if !f(torrent) { - // return false - // } - // } - // return true - // } - - // tr := []*model.Torrent{} - // for _, t := range torrents { - // d, err := model.MapTorrent(ctx, t) - // if err != nil { - // return nil, err - // } - - // if !filterFunc(d) { - // continue - // } - // tr = append(tr, d) - // } - - // slices.SortStableFunc(torrents, func(t1, t2 *torrent.Controller) int { - // return strings.Compare(t1.Name(), t2.Name()) - // }) - - // return tr, nil - panic("not implemented") -} - -// ClientStats is the resolver for the clientStats field. -func (r *torrentDaemonQueryResolver) ClientStats(ctx context.Context, obj *model.TorrentDaemonQuery) (*model.TorrentClientStats, error) { - // stats := r.ATorrentDaemon.Stats() - // return &model.TorrentClientStats{ - // BytesWritten: stats.BytesWritten.Int64(), - // BytesRead: stats.BytesRead.Int64(), - // BytesWrittenData: stats.BytesWrittenData.Int64(), - // BytesReadData: stats.BytesReadData.Int64(), - // BytesReadUsefulData: stats.BytesReadUsefulData.Int64(), - // BytesReadUsefulIntendedData: stats.BytesReadUsefulIntendedData.Int64(), - // ChunksWritten: stats.ChunksWritten.Int64(), - // ChunksRead: stats.ChunksRead.Int64(), - // ChunksReadUseful: stats.ChunksReadUseful.Int64(), - // ChunksReadWasted: stats.ChunksReadWasted.Int64(), - // MetadataChunksRead: stats.MetadataChunksRead.Int64(), - // PiecesDirtiedGood: stats.PiecesDirtiedGood.Int64(), - // PiecesDirtiedBad: stats.PiecesDirtiedBad.Int64(), - // }, nil - panic("not implemented") -} - -// StatsHistory is the resolver for the statsHistory field. -func (r *torrentDaemonQueryResolver) StatsHistory(ctx context.Context, obj *model.TorrentDaemonQuery, since time.Time, infohash *string) ([]*model.TorrentStats, error) { - // var stats []torrent.TorrentStats - // if infohash == nil { - // stats, err := r.ATorrentDaemon.StatsHistory(ctx, since) - // if err != nil { - // return nil, err - // } - // return model.Apply(stats, model.MapTorrentStats), nil - // } else if *infohash == "total" { - // var err error - // stats, err = r.ATorrentDaemon.TotalStatsHistory(ctx, since) - // if err != nil { - // return nil, err - // } - // } else { - // ih := tinfohash.FromHexString(*infohash) - // var err error - // stats, err = r.ATorrentDaemon.TorrentStatsHistory(ctx, since, ih) - // if err != nil { - // return nil, err - // } - // } - - // return model.Apply(stats, model.MapTorrentStats), nil - panic("not implemented") -} - -// TorrentDaemonQuery returns graph.TorrentDaemonQueryResolver implementation. -func (r *Resolver) TorrentDaemonQuery() graph.TorrentDaemonQueryResolver { - return &torrentDaemonQueryResolver{r} -} - -type torrentDaemonQueryResolver struct{ *Resolver } diff --git a/daemons/atorrent/delivery/graphql/torrent_types.resolvers.go b/daemons/atorrent/delivery/graphql/torrent_types.resolvers.go deleted file mode 100644 index bce3b03..0000000 --- a/daemons/atorrent/delivery/graphql/torrent_types.resolvers.go +++ /dev/null @@ -1,90 +0,0 @@ -package graphql - -// This file will be automatically regenerated based on the schema, any resolver implementations -// will be copied through when generating and any unknown code will be moved to the end. -// Code generated by github.com/99designs/gqlgen version v0.17.55 - -import ( - "context" - - graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql" - "git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model" - "github.com/anacrolix/torrent/types" -) - -// Name is the resolver for the name field. -func (r *torrentResolver) Name(ctx context.Context, obj *model.Torrent) (string, error) { - return obj.T.Name(), nil -} - -// Files is the resolver for the files field. -func (r *torrentResolver) Files(ctx context.Context, obj *model.Torrent) ([]*model.TorrentFile, error) { - out := []*model.TorrentFile{} - files, err := obj.T.Files(ctx) - if err != nil { - return nil, err - } - for _, f := range files { - out = append(out, &model.TorrentFile{ - Filename: f.Path(), - Size: f.Size(), - BytesCompleted: f.BytesCompleted(), - F: f, - }) - } - return out, nil -} - -// ExcludedFiles is the resolver for the excludedFiles field. -func (r *torrentResolver) ExcludedFiles(ctx context.Context, obj *model.Torrent) ([]*model.TorrentFile, error) { - out := []*model.TorrentFile{} - // files, err := obj.T.ExcludedFiles() - // if err != nil { - // return nil, err - // } - // for _, f := range files { - // out = append(out, &model.TorrentFile{ - // Filename: f.DisplayPath(), - // Size: f.Length(), - // F: f, - // }) - // } - return out, nil -} - -// Peers is the resolver for the peers field. -func (r *torrentResolver) Peers(ctx context.Context, obj *model.Torrent) ([]*model.TorrentPeer, error) { - peers := []*model.TorrentPeer{} - for _, peer := range obj.T.Torrent().PeerConns() { - clientName, _ := peer.PeerClientName.Load().(string) - - peers = append(peers, &model.TorrentPeer{ - IP: peer.RemoteAddr.String(), - DownloadRate: peer.DownloadRate(), - - Discovery: model.MapPeerSource(peer.Discovery), - Port: int64(peer.PeerListenPort), - ClientName: clientName, - F: peer, - }) - } - return peers, nil -} - -// Priority is the resolver for the priority field. -func (r *torrentFileResolver) Priority(ctx context.Context, obj *model.TorrentFile) (types.PiecePriority, error) { - props, err := obj.F.Properties(ctx) - if err != nil { - return 0, err - } - return props.Priority, nil -} - -// Torrent returns graph.TorrentResolver implementation. -func (r *Resolver) Torrent() graph.TorrentResolver { return &torrentResolver{r} } - -// TorrentFile returns graph.TorrentFileResolver implementation. -func (r *Resolver) TorrentFile() graph.TorrentFileResolver { return &torrentFileResolver{r} } - -type torrentResolver struct{ *Resolver } -type torrentFileResolver struct{ *Resolver } diff --git a/daemons/atorrent/dht_fileitem_store.go b/daemons/atorrent/dht_fileitem_store.go deleted file mode 100644 index 9358e83..0000000 --- a/daemons/atorrent/dht_fileitem_store.go +++ /dev/null @@ -1,112 +0,0 @@ -package atorrent - -import ( - "bytes" - "encoding/gob" - "time" - - "git.kmsign.ru/royalcat/tstor/src/logwrap" - "github.com/anacrolix/dht/v2/bep44" - "github.com/dgraph-io/badger/v4" -) - -var _ bep44.Store = &dhtFileItemStore{} - -type dhtFileItemStore struct { - ttl time.Duration - db *badger.DB -} - -func newDHTStore(path string, itemsTTL time.Duration) (*dhtFileItemStore, error) { - opts := badger.DefaultOptions(path). - WithLogger(logwrap.BadgerLogger("torrent-client", "dht-item-store")). - WithValueLogFileSize(1<<26 - 1) - - db, err := badger.Open(opts) - if err != nil { - return nil, err - } - - err = db.RunValueLogGC(0.5) - if err != nil && err != badger.ErrNoRewrite { - return nil, err - } - - return &dhtFileItemStore{ - db: db, - ttl: itemsTTL, - }, nil -} - -func (fis *dhtFileItemStore) Put(i *bep44.Item) error { - tx := fis.db.NewTransaction(true) - defer tx.Discard() - - key := i.Target() - var value bytes.Buffer - - enc := gob.NewEncoder(&value) - if err := enc.Encode(i); err != nil { - return err - } - - e := badger.NewEntry(key[:], value.Bytes()).WithTTL(fis.ttl) - if err := tx.SetEntry(e); err != nil { - return err - } - - return tx.Commit() -} - -func (fis *dhtFileItemStore) Get(t bep44.Target) (*bep44.Item, error) { - tx := fis.db.NewTransaction(false) - defer tx.Discard() - - dbi, err := tx.Get(t[:]) - if err == badger.ErrKeyNotFound { - return nil, bep44.ErrItemNotFound - } - if err != nil { - return nil, err - } - valb, err := dbi.ValueCopy(nil) - if err != nil { - return nil, err - } - - buf := bytes.NewBuffer(valb) - dec := gob.NewDecoder(buf) - var i *bep44.Item - if err := dec.Decode(&i); err != nil { - return nil, err - } - - return i, nil -} - -func (fis *dhtFileItemStore) Del(t bep44.Target) error { - tx := fis.db.NewTransaction(true) - defer tx.Discard() - - err := tx.Delete(t[:]) - if err == badger.ErrKeyNotFound { - return nil - } - if err != nil { - return err - } - - err = tx.Commit() - if err == badger.ErrKeyNotFound { - return nil - } - if err != nil { - return err - } - - return nil -} - -func (fis *dhtFileItemStore) Close() error { - return fis.db.Close() -} diff --git a/daemons/atorrent/dup_cache.go b/daemons/atorrent/dup_cache.go deleted file mode 100644 index fc400d2..0000000 --- a/daemons/atorrent/dup_cache.go +++ /dev/null @@ -1,92 +0,0 @@ -package atorrent - -import ( - "path" - "slices" - "sync" - - "git.kmsign.ru/royalcat/tstor/pkg/slicesutils" - "github.com/anacrolix/torrent/metainfo" - "github.com/anacrolix/torrent/types/infohash" -) - -type dupInfo struct { - infohash infohash.T - fileinfo metainfo.FileInfo -} - -type dupIndex struct { - mu sync.RWMutex - torrents map[infohash.T][]metainfo.FileInfo - sha1 map[string][]dupInfo // bittorrent v1 - piecesRoot map[[32]byte][]dupInfo // bittorrent v2 -} - -func newDupIndex() *dupIndex { - return &dupIndex{ - torrents: map[infohash.T][]metainfo.FileInfo{}, - sha1: map[string][]dupInfo{}, - piecesRoot: map[[32]byte][]dupInfo{}, - } -} - -func (c *dupIndex) AddFile(fileinfo metainfo.FileInfo, ih infohash.T) { - c.mu.Lock() - defer c.mu.Unlock() - - c.torrents[ih] = append(c.torrents[ih], fileinfo) - - if fileinfo.Sha1 != "" { - c.sha1[fileinfo.Sha1] = append(c.sha1[fileinfo.Sha1], dupInfo{fileinfo: fileinfo, infohash: ih}) - } - - if fileinfo.PiecesRoot.Ok { - c.piecesRoot[fileinfo.PiecesRoot.Value] = append(c.piecesRoot[fileinfo.PiecesRoot.Value], dupInfo{fileinfo: fileinfo, infohash: ih}) - } -} - -func (c *dupIndex) DuplicateFiles(fileinfo metainfo.FileInfo, ih infohash.T) []dupInfo { - c.mu.RLock() - defer c.mu.RUnlock() - - if fileinfo.Sha1 != "" { - if dups, ok := c.sha1[fileinfo.Sha1]; ok { - return slices.Clone(dups) - } - } - - if fileinfo.PiecesRoot.Ok { - if dups, ok := c.piecesRoot[fileinfo.PiecesRoot.Value]; ok { - return slices.Clone(dups) - } - } - - return []dupInfo{} -} - -func (c *dupIndex) Includes(ih infohash.T, files []metainfo.FileInfo) []dupInfo { - c.mu.RLock() - defer c.mu.RUnlock() - - out := []dupInfo{} - - for ih, v := range c.torrents { - intersection := slicesutils.IntersectionFunc(files, v, func(a, b metainfo.FileInfo) bool { - mostly := path.Join(a.BestPath()...) == path.Join(b.BestPath()...) && a.Length == b.Length - if a.Sha1 != "" && b.Sha1 != "" { - return mostly && a.Sha1 == b.Sha1 - } - if a.PiecesRoot.Ok && b.PiecesRoot.Ok { - return mostly && a.PiecesRoot.Value == b.PiecesRoot.Value - } - - return mostly - }) - - for _, v := range intersection { - out = append(out, dupInfo{infohash: ih, fileinfo: v}) - } - } - - return []dupInfo{} -} diff --git a/daemons/atorrent/file_controller.go b/daemons/atorrent/file_controller.go deleted file mode 100644 index 9169a18..0000000 --- a/daemons/atorrent/file_controller.go +++ /dev/null @@ -1,99 +0,0 @@ -package atorrent - -import ( - "context" - "log/slog" - - "git.kmsign.ru/royalcat/tstor/pkg/kvsingle" - "git.kmsign.ru/royalcat/tstor/pkg/rlog" - "github.com/anacrolix/torrent" - "github.com/anacrolix/torrent/metainfo" - "github.com/anacrolix/torrent/types" - "github.com/royalcat/kv" -) - -type FileController struct { - file *torrent.File - properties *kvsingle.Value[string, FileProperties] - log *rlog.Logger -} - -func NewFileController(f *torrent.File, properties *kvsingle.Value[string, FileProperties], log *rlog.Logger) *FileController { - return &FileController{ - file: f, - properties: properties, - log: log.WithComponent("file-controller").With(slog.String("file", f.Path())), - } -} - -func (s *FileController) Properties(ctx context.Context) (FileProperties, error) { - p, err := s.properties.Get(ctx) - if err == kv.ErrKeyNotFound { - return FileProperties{ - Excluded: false, - Priority: defaultPriority, - }, nil - } - - if err != nil { - return FileProperties{}, err - } - return p, nil -} - -func (s *FileController) SetPriority(ctx context.Context, priority types.PiecePriority) error { - log := s.log.With(slog.Int("priority", int(priority))) - - err := s.properties.Edit(ctx, func(ctx context.Context, v FileProperties) (FileProperties, error) { - v.Priority = priority - return v, nil - }) - if err == kv.ErrKeyNotFound { - seterr := s.properties.Set(ctx, FileProperties{ - Priority: priority, - }) - if seterr != nil { - return err - } - err = nil - } - if err != nil { - return err - } - - log.Debug(ctx, "file priority set") - s.file.SetPriority(priority) - - return nil -} - -func (s *FileController) FileInfo() metainfo.FileInfo { - return s.file.FileInfo() -} - -func (s *FileController) Excluded(ctx context.Context) (bool, error) { - p, err := s.properties.Get(ctx) - if err == kv.ErrKeyNotFound { - return false, nil - } - if err != nil { - return false, err - } - return p.Excluded, nil -} - -func (s *FileController) Path() string { - return s.file.Path() -} - -func (s *FileController) Size() int64 { - return s.file.Length() -} - -func (s *FileController) Priority() types.PiecePriority { - return s.file.Priority() -} - -func (s *FileController) BytesCompleted() int64 { - return s.file.BytesCompleted() -} diff --git a/daemons/atorrent/fs.go b/daemons/atorrent/fs.go deleted file mode 100644 index 4c33003..0000000 --- a/daemons/atorrent/fs.go +++ /dev/null @@ -1,567 +0,0 @@ -package atorrent - -import ( - "context" - "fmt" - "io" - "io/fs" - "log/slog" - "path" - "slices" - "strings" - "sync" - "sync/atomic" - "time" - - "git.kmsign.ru/royalcat/tstor/pkg/rlog" - "git.kmsign.ru/royalcat/tstor/src/vfs" - "github.com/anacrolix/torrent" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "golang.org/x/exp/maps" -) - -type TorrentFS struct { - name string - - Torrent *Controller - - filesCacheMu sync.Mutex - filesCache map[string]vfs.File - - lastTorrentReadTimeout atomic.Pointer[time.Time] - - resolver *vfs.Resolver -} - -var _ vfs.Filesystem = (*TorrentFS)(nil) - -const shortTimeout = time.Millisecond -const lowTimeout = time.Second * 5 - -func (s *Daemon) NewTorrentFs(ctx context.Context, _ string, f vfs.File) (vfs.Filesystem, error) { - c, err := s.loadTorrent(ctx, f) - if err != nil { - return nil, err - } - - if err := f.Close(ctx); err != nil { - s.log.Error(ctx, "failed to close file", slog.String("name", f.Name()), rlog.Error(err)) - } - - return &TorrentFS{ - name: f.Name(), - Torrent: c, - resolver: vfs.NewResolver(vfs.ArchiveFactories), - }, nil -} - -var _ fs.DirEntry = (*TorrentFS)(nil) - -// Name implements fs.DirEntry. -func (tfs *TorrentFS) Name() string { - return tfs.name -} - -// Info implements fs.DirEntry. -func (tfs *TorrentFS) Info() (fs.FileInfo, error) { - return tfs, nil -} - -// IsDir implements fs.DirEntry. -func (tfs *TorrentFS) IsDir() bool { - return true -} - -// Type implements fs.DirEntry. -func (tfs *TorrentFS) Type() fs.FileMode { - return fs.ModeDir -} - -// ModTime implements fs.FileInfo. -func (tfs *TorrentFS) ModTime() time.Time { - return time.Time{} -} - -// Mode implements fs.FileInfo. -func (tfs *TorrentFS) Mode() fs.FileMode { - return fs.ModeDir -} - -// Size implements fs.FileInfo. -func (tfs *TorrentFS) Size() int64 { - return 0 -} - -// Sys implements fs.FileInfo. -func (tfs *TorrentFS) Sys() any { - return nil -} - -// FsName implements Filesystem. -func (tfs *TorrentFS) FsName() string { - return "torrentfs" -} - -func (fs *TorrentFS) files(ctx context.Context) (map[string]vfs.File, error) { - fs.filesCacheMu.Lock() - defer fs.filesCacheMu.Unlock() - - if fs.filesCache != nil { - return fs.filesCache, nil - } - - ctx, span := tracer.Start(ctx, "files", fs.traceAttrs()) - defer span.End() - - files, err := fs.Torrent.Files(ctx) - if err != nil { - return nil, err - } - - fs.filesCache = make(map[string]vfs.File) - for _, file := range files { - props, err := file.Properties(ctx) - if err != nil { - return nil, err - } - if props.Excluded { - continue - } - - p := vfs.AbsPath(file.Path()) - tf, err := openTorrentFile(ctx, path.Base(p), file.file, &fs.lastTorrentReadTimeout) - if err != nil { - return nil, err - } - fs.filesCache[p] = tf - } - - // TODO optional - // if len(fs.filesCache) == 1 && fs.resolver.IsNestedFs(fs.Torrent.Name()) { - // filepath := "/" + fs.Torrent.Name() - // if file, ok := fs.filesCache[filepath]; ok { - // nestedFs, err := fs.resolver.NestedFs(ctx, filepath, file) - // if err != nil { - // return nil, err - // } - // if nestedFs == nil { - // goto DEFAULT_DIR // FIXME - // } - // fs.filesCache, err = listFilesRecursive(ctx, nestedFs, "/") - // if err != nil { - // return nil, err - // } - - // return fs.filesCache, nil - // } - // } - // DEFAULT_DIR: - - rootDir := "/" + fs.Torrent.Name() + "/" - singleDir := true - for k, _ := range fs.filesCache { - if !strings.HasPrefix(k, rootDir) { - singleDir = false - } - } - if singleDir { - for k, f := range fs.filesCache { - delete(fs.filesCache, k) - k, _ = strings.CutPrefix(k, rootDir) - k = vfs.AbsPath(k) - fs.filesCache[k] = f - } - } - - return fs.filesCache, nil -} - -func listFilesRecursive(ctx context.Context, fs vfs.Filesystem, start string) (map[string]vfs.File, error) { - out := make(map[string]vfs.File, 0) - entries, err := fs.ReadDir(ctx, start) - if err != nil { - return nil, err - } - for _, entry := range entries { - filename := path.Join(start, entry.Name()) - if entry.IsDir() { - rec, err := listFilesRecursive(ctx, fs, filename) - if err != nil { - return nil, err - } - maps.Copy(out, rec) - } else { - file, err := fs.Open(ctx, filename) - if err != nil { - return nil, err - } - out[filename] = file - } - } - - return out, nil -} - -func (fs *TorrentFS) rawOpen(ctx context.Context, filename string) (file vfs.File, err error) { - ctx, span := tracer.Start(ctx, "rawOpen", - fs.traceAttrs(attribute.String("filename", filename)), - ) - defer func() { - if err != nil { - span.RecordError(err) - } - span.End() - }() - - files, err := fs.files(ctx) - if err != nil { - return nil, err - } - file, err = vfs.GetFile(files, filename) - return file, err -} - -func (fs *TorrentFS) rawStat(ctx context.Context, filename string) (fs.FileInfo, error) { - ctx, span := tracer.Start(ctx, "rawStat", - fs.traceAttrs(attribute.String("filename", filename)), - ) - defer span.End() - - files, err := fs.files(ctx) - if err != nil { - return nil, err - } - - file, err := vfs.GetFile(files, filename) - if err != nil { - return nil, err - } - return file.Info() -} - -func (fs *TorrentFS) traceAttrs(add ...attribute.KeyValue) trace.SpanStartOption { - return trace.WithAttributes(append([]attribute.KeyValue{ - attribute.String("fs", fs.FsName()), - attribute.String("torrent", fs.Torrent.Name()), - attribute.String("infohash", fs.Torrent.InfoHash()), - }, add...)...) -} - -func (tfs *TorrentFS) readContext(ctx context.Context) (context.Context, context.CancelFunc) { - lastReadTimeout := tfs.lastTorrentReadTimeout.Load() - if lastReadTimeout != nil && time.Since(*lastReadTimeout) < secondaryTimeout { // make short timeout for already faliled files - trace.SpanFromContext(ctx).SetAttributes(attribute.Bool("short_timeout", true)) - - return context.WithTimeout(ctx, shortTimeout) - } - - return ctx, func() {} -} - -// Stat implements Filesystem. -func (tfs *TorrentFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) { - ctx, span := tracer.Start(ctx, "Stat", - tfs.traceAttrs(attribute.String("filename", filename)), - ) - defer span.End() - - if vfs.IsRoot(filename) { - return tfs, nil - } - - var err error - ctx, cancel := tfs.readContext(ctx) - defer func() { - cancel() - if err == context.DeadlineExceeded { - now := time.Now() - tfs.lastTorrentReadTimeout.Store(&now) - } - }() - - fsPath, nestedFs, nestedFsPath, err := tfs.resolver.ResolvePath(ctx, filename, tfs.rawOpen) - if err != nil { - return nil, err - } - if nestedFs != nil { - return nestedFs.Stat(ctx, nestedFsPath) - } - - return tfs.rawStat(ctx, fsPath) -} - -func (tfs *TorrentFS) Open(ctx context.Context, filename string) (file vfs.File, err error) { - ctx, span := tracer.Start(ctx, "Open", - tfs.traceAttrs(attribute.String("filename", filename)), - ) - defer span.End() - - if vfs.IsRoot(filename) { - return vfs.NewDirFile(tfs.name), nil - } - - ctx, cancel := tfs.readContext(ctx) - defer func() { - cancel() - if err == context.DeadlineExceeded { - now := time.Now() - tfs.lastTorrentReadTimeout.Store(&now) - } - }() - - fsPath, nestedFs, nestedFsPath, err := tfs.resolver.ResolvePath(ctx, filename, tfs.rawOpen) - if err != nil { - return nil, err - } - if nestedFs != nil { - return nestedFs.Open(ctx, nestedFsPath) - } - - return tfs.rawOpen(ctx, fsPath) -} - -func (tfs *TorrentFS) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) { - ctx, span := tracer.Start(ctx, "ReadDir", - tfs.traceAttrs(attribute.String("name", name)), - ) - defer span.End() - - var err error - ctx, cancel := tfs.readContext(ctx) - defer func() { - cancel() - if err == context.DeadlineExceeded { - now := time.Now() - tfs.lastTorrentReadTimeout.Store(&now) - } - }() - - fsPath, nestedFs, nestedFsPath, err := tfs.resolver.ResolvePath(ctx, name, tfs.rawOpen) - if err != nil { - return nil, err - } - if nestedFs != nil { - return nestedFs.ReadDir(ctx, nestedFsPath) - } - files, err := tfs.files(ctx) - if err != nil { - return nil, err - } - - return vfs.ListDirFromFiles(files, fsPath) -} - -func (fs *TorrentFS) Unlink(ctx context.Context, name string) error { - ctx, span := tracer.Start(ctx, "Unlink", - fs.traceAttrs(attribute.String("name", name)), - ) - defer span.End() - - name = vfs.AbsPath(name) - - files, err := fs.files(ctx) - if err != nil { - return err - } - - if !slices.Contains(maps.Keys(files), name) { - return vfs.ErrNotExist - } - - file := files[name] - - fs.filesCacheMu.Lock() - delete(fs.filesCache, name) - fs.filesCacheMu.Unlock() - - tfile, ok := file.(*torrentFile) - if !ok { - return vfs.ErrNotImplemented - } - - return fs.Torrent.ExcludeFile(ctx, tfile.file) -} - -// Rename implements vfs.Filesystem. -func (s *TorrentFS) Rename(ctx context.Context, oldpath string, newpath string) error { - return vfs.ErrNotImplemented -} - -var _ vfs.File = (*torrentFile)(nil) - -type torrentFile struct { - name string - - mu sync.RWMutex - - tr torrent.Reader - - lastReadTimeout atomic.Pointer[time.Time] - lastTorrentReadTimeout *atomic.Pointer[time.Time] - - file *torrent.File -} - -const secondaryTimeout = time.Hour * 24 - -func openTorrentFile(ctx context.Context, name string, file *torrent.File, lastTorrentReadTimeout *atomic.Pointer[time.Time]) (*torrentFile, error) { - select { - case <-file.Torrent().GotInfo(): - break - case <-ctx.Done(): - return nil, ctx.Err() - } - - r := file.NewReader() - _, err := r.ReadContext(ctx, make([]byte, 128)) - if err != nil && err != io.EOF { - return nil, fmt.Errorf("failed initial file read: %w", err) - } - _, err = r.Seek(0, io.SeekStart) - if err != nil { - return nil, fmt.Errorf("failed seeking to start, after initial read: %w", err) - } - - return &torrentFile{ - name: name, - tr: r, - file: file, - lastTorrentReadTimeout: lastTorrentReadTimeout, - }, nil -} - -// Name implements File. -func (tf *torrentFile) Name() string { - return tf.name -} - -// Seek implements vfs.File. -func (tf *torrentFile) Seek(offset int64, whence int) (int64, error) { - tf.mu.Lock() - defer tf.mu.Unlock() - - return tf.tr.Seek(offset, whence) -} - -// Type implements File. -func (tf *torrentFile) Type() fs.FileMode { - return vfs.ModeFileRO | fs.ModeDir -} - -func (tf *torrentFile) Info() (fs.FileInfo, error) { - return vfs.NewFileInfo(tf.name, tf.file.Length(), time.Time{}), nil -} - -func (tf *torrentFile) Size() int64 { - return tf.file.Length() -} - -func (tf *torrentFile) IsDir() bool { - return false -} - -func (rw *torrentFile) Close(ctx context.Context) error { - rw.mu.Lock() - defer rw.mu.Unlock() - - return rw.tr.Close() -} - -func (tf *torrentFile) readTimeout(ctx context.Context) (context.Context, context.CancelFunc) { - lastReadTimeout := tf.lastReadTimeout.Load() - if lastReadTimeout != nil && time.Since(*lastReadTimeout) < secondaryTimeout { // make short timeout for already faliled files - trace.SpanFromContext(ctx).SetAttributes(attribute.Bool("short_timeout", true)) - return context.WithTimeout(ctx, shortTimeout) - } - - lastTorrentReadTimeout := tf.lastTorrentReadTimeout.Load() - if lastTorrentReadTimeout != nil && time.Since(*lastTorrentReadTimeout) < secondaryTimeout { // make short timeout for already faliled files - trace.SpanFromContext(ctx).SetAttributes(attribute.Bool("low_timeout", true)) - return context.WithTimeout(ctx, lowTimeout) - } - - return ctx, func() {} -} - -// Read implements ctxio.Reader. -func (tf *torrentFile) Read(ctx context.Context, p []byte) (n int, err error) { - ctx, span := tracer.Start(ctx, "Read", - trace.WithAttributes(attribute.Int("length", len(p))), - ) - defer func() { - span.SetAttributes(attribute.Int("read", n)) - span.End() - }() - - tf.mu.Lock() - defer tf.mu.Unlock() - - ctx, cancel := tf.readTimeout(ctx) - defer cancel() - defer func() { - if err == context.DeadlineExceeded { - now := time.Now() - tf.lastReadTimeout.Store(&now) - tf.lastTorrentReadTimeout.Store(&now) - } - }() - - return tf.tr.ReadContext(ctx, p) -} - -func (tf *torrentFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) { - ctx, span := tracer.Start(ctx, "ReadAt", - trace.WithAttributes(attribute.Int("length", len(p)), attribute.Int64("offset", off)), - ) - defer func() { - span.SetAttributes(attribute.Int("read", n)) - span.End() - }() - - tf.mu.RLock() - defer tf.mu.RUnlock() - - ctx, cancel := tf.readTimeout(ctx) - defer cancel() - defer func() { - if err == context.DeadlineExceeded { - now := time.Now() - tf.lastReadTimeout.Store(&now) - tf.lastTorrentReadTimeout.Store(&now) - } - }() - - _, err = tf.tr.Seek(off, io.SeekStart) - if err != nil { - return 0, err - } - - // return tf.tr.ReadContext(ctx, p) - n, err = readAtLeast(ctx, tf.tr, p, len(p)) - - _, err = tf.tr.Seek(0, io.SeekStart) - if err != nil { - return 0, err - } - - return n, err -} - -func readAtLeast(ctx context.Context, r torrent.Reader, buf []byte, min int) (n int, err error) { - if len(buf) < min { - return 0, io.ErrShortBuffer - } - for n < min && err == nil { - var nn int - - nn, err = r.ReadContext(ctx, buf[n:]) - n += nn - } - if n >= min { - err = nil - } else if n > 0 && err == io.EOF { - err = io.ErrUnexpectedEOF - } - return -} diff --git a/daemons/atorrent/fs_test.go b/daemons/atorrent/fs_test.go deleted file mode 100644 index b1c7765..0000000 --- a/daemons/atorrent/fs_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package atorrent - -import ( - "os" - "testing" - - "github.com/anacrolix/torrent" -) - -const testMagnet = "magnet:?xt=urn:btih:a88fda5954e89178c372716a6a78b8180ed4dad3&dn=The+WIRED+CD+-+Rip.+Sample.+Mash.+Share&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fwired-cd.torrent" - -var Cli *torrent.Client - -func TestMain(m *testing.M) { - cfg := torrent.NewDefaultClientConfig() - cfg.DataDir = os.TempDir() - - // disable webseeds to avoid a panic when closing client on tests - cfg.DisableWebseeds = true - - client, err := torrent.NewClient(cfg) - if err != nil { - panic(err) - } - - Cli = client - - exitVal := m.Run() - - client.Close() - - os.Exit(exitVal) -} - -// func TestTorrentFilesystem(t *testing.T) { -// require := require.New(t) - -// to, err := Cli.AddMagnet(testMagnet) -// require.NoError(err) - -// tfs := NewTorrentFs(600) -// tfs.AddTorrent(to) - -// files, err := tfs.ReadDir("/") -// require.NoError(err) -// require.Len(files, 1) -// require.Contains(files, "The WIRED CD - Rip. Sample. Mash. Share") - -// files, err = tfs.ReadDir("/The WIRED CD - Rip. Sample. Mash. Share") -// require.NoError(err) -// require.Len(files, 18) - -// f, err := tfs.Open("/The WIRED CD - Rip. Sample. Mash. Share/not_existing_file.txt") -// require.Equal(os.ErrNotExist, err) -// require.Nil(f) - -// f, err = tfs.Open("/The WIRED CD - Rip. Sample. Mash. Share/01 - Beastie Boys - Now Get Busy.mp3") -// require.NoError(err) -// require.NotNil(f) -// require.Equal(f.Size(), int64(1964275)) - -// b := make([]byte, 10) - -// n, err := f.Read(b) -// require.NoError(err) -// require.Equal(10, n) -// require.Equal([]byte{0x49, 0x44, 0x33, 0x3, 0x0, 0x0, 0x0, 0x0, 0x1f, 0x76}, b) - -// n, err = f.ReadAt(b, 10) -// require.NoError(err) -// require.Equal(10, n) - -// n, err = f.ReadAt(b, 10000) -// require.NoError(err) -// require.Equal(10, n) - -// tfs.RemoveTorrent(to.InfoHash().String()) -// files, err = tfs.ReadDir("/") -// require.NoError(err) -// require.Len(files, 0) - -// require.NoError(f.Close()) -// } - -// func TestReadAtTorrent(t *testing.T) { -// t.Parallel() - -// ctx := context.Background() - -// require := require.New(t) - -// to, err := Cli.AddMagnet(testMagnet) -// require.NoError(err) - -// <-to.GotInfo() -// torrFile := to.Files()[0] - -// tf, err := openTorrentFile(ctx, "torr", torrFile) -// require.NoError(err) - -// defer tf.Close(ctx) - -// toRead := make([]byte, 5) -// n, err := tf.ReadAt(ctx, toRead, 6) -// require.NoError(err) -// require.Equal(5, n) -// require.Equal([]byte{0x0, 0x0, 0x1f, 0x76, 0x54}, toRead) - -// n, err = tf.ReadAt(ctx, toRead, 0) -// require.NoError(err) -// require.Equal(5, n) -// require.Equal([]byte{0x49, 0x44, 0x33, 0x3, 0x0}, toRead) -// } - -// func TestReadAtWrapper(t *testing.T) { -// t.Parallel() - -// ctx := context.Background() - -// require := require.New(t) - -// to, err := Cli.AddMagnet(testMagnet) -// require.NoError(err) - -// <-to.GotInfo() -// torrFile := to.Files()[0] - -// r, err := openTorrentFile(ctx, "file", torrFile) -// require.NoError(err) -// defer r.Close(ctx) - -// toRead := make([]byte, 5) -// n, err := r.ReadAt(ctx, toRead, 6) -// require.NoError(err) -// require.Equal(5, n) -// require.Equal([]byte{0x0, 0x0, 0x1f, 0x76, 0x54}, toRead) - -// n, err = r.ReadAt(ctx, toRead, 0) -// require.NoError(err) -// require.Equal(5, n) -// require.Equal([]byte{0x49, 0x44, 0x33, 0x3, 0x0}, toRead) -// } diff --git a/daemons/atorrent/go.mod b/daemons/atorrent/go.mod deleted file mode 100644 index bf13c0a..0000000 --- a/daemons/atorrent/go.mod +++ /dev/null @@ -1,136 +0,0 @@ -module git.kmsign.ru/royalcat/tstor/daemons/atorrent - -go 1.23.5 - -require ( - git.kmsign.ru/royalcat/tstor v0.0.0-20250120032914-a43371d1e3b9 - github.com/anacrolix/dht/v2 v2.22.0 - github.com/anacrolix/log v0.16.0 - github.com/anacrolix/torrent v1.58.1-0.20241228235504-75e6b6565845 - github.com/dgraph-io/badger/v4 v4.5.0 - github.com/dustin/go-humanize v1.0.1 - github.com/go-git/go-billy/v5 v5.6.1 - github.com/royalcat/ctxio v0.0.0-20240602084623-009bd79b3176 - github.com/royalcat/kv v0.0.0-20240723215915-954e36a2491d - github.com/royalcat/kv/kvbadger v0.0.0-20240723215915-954e36a2491d - github.com/stretchr/testify v1.10.0 - go.opentelemetry.io/otel v1.34.0 - go.opentelemetry.io/otel/metric v1.34.0 - go.opentelemetry.io/otel/trace v1.34.0 - golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 - golang.org/x/sys v0.29.0 -) - -require ( - github.com/99designs/gqlgen v0.17.55 // indirect - github.com/RoaringBitmap/roaring v1.2.3 // indirect - github.com/agnivade/levenshtein v1.1.1 // indirect - github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect - github.com/alecthomas/assert/v2 v2.3.0 // indirect - github.com/alecthomas/atomic v0.1.0-alpha2 // indirect - github.com/anacrolix/chansync v0.4.1-0.20240627045151-1aa1ac392fe8 // indirect - github.com/anacrolix/envpprof v1.3.0 // indirect - github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect - github.com/anacrolix/go-libutp v1.3.1 // indirect - github.com/anacrolix/missinggo v1.3.0 // indirect - github.com/anacrolix/missinggo/perf v1.0.0 // indirect - github.com/anacrolix/missinggo/v2 v2.7.4 // indirect - github.com/anacrolix/mmsg v1.0.0 // indirect - github.com/anacrolix/multiless v0.4.0 // indirect - github.com/anacrolix/stm v0.4.1-0.20221221005312-96d17df0e496 // indirect - github.com/anacrolix/sync v0.5.1 // indirect - github.com/anacrolix/upnp v0.1.4 // indirect - github.com/anacrolix/utp v0.1.0 // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d // indirect - github.com/bits-and-blooms/bitset v1.2.2 // indirect - github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect - github.com/cespare/xxhash v1.1.0 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgraph-io/ristretto/v2 v2.0.0 // indirect - github.com/edsrzf/mmap-go v1.1.0 // indirect - github.com/fatih/structs v1.1.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect - github.com/go-llsqlite/crawshaw v0.5.2-0.20240425034140-f30eb7704568 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/gofrs/uuid/v5 v5.1.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/btree v1.1.2 // indirect - github.com/google/flatbuffers v24.3.25+incompatible // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.1 // indirect - github.com/goware/singleflight v0.2.0 // indirect - github.com/huandu/xstrings v1.4.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect - github.com/knadh/koanf/maps v0.1.1 // indirect - github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect - github.com/knadh/koanf/providers/env v0.1.0 // indirect - github.com/knadh/koanf/providers/file v0.1.0 // indirect - github.com/knadh/koanf/providers/structs v0.1.0 // indirect - github.com/knadh/koanf/v2 v2.1.2 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/minio/sha256-simd v1.0.0 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect - github.com/mschoch/smat v0.2.0 // indirect - github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-varint v0.0.6 // indirect - github.com/pion/datachannel v1.5.9 // indirect - github.com/pion/dtls/v3 v3.0.3 // indirect - github.com/pion/ice/v4 v4.0.2 // indirect - github.com/pion/interceptor v0.1.37 // indirect - github.com/pion/logging v0.2.2 // indirect - github.com/pion/mdns/v2 v2.0.7 // indirect - github.com/pion/randutil v0.1.0 // indirect - github.com/pion/rtcp v1.2.14 // indirect - github.com/pion/rtp v1.8.9 // indirect - github.com/pion/sctp v1.8.33 // indirect - github.com/pion/sdp/v3 v3.0.9 // indirect - github.com/pion/srtp/v3 v3.0.4 // indirect - github.com/pion/stun/v3 v3.0.0 // indirect - github.com/pion/transport/v3 v3.0.7 // indirect - github.com/pion/turn/v4 v4.0.0 // indirect - github.com/pion/webrtc/v4 v4.0.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/protolambda/ctxlock v0.1.0 // indirect - github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/royalcat/btrgo v0.0.0-20240318160410-19bd27154450 // indirect - github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect - github.com/rs/zerolog v1.32.0 // indirect - github.com/samber/lo v1.47.0 // indirect - github.com/samber/slog-multi v1.0.2 // indirect - github.com/samber/slog-zerolog v1.0.0 // indirect - github.com/sosodev/duration v1.3.1 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/tidwall/btree v1.6.0 // indirect - github.com/vektah/gqlparser/v2 v2.5.17 // indirect - github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 // indirect - github.com/wlynxg/anet v0.0.3 // indirect - go.etcd.io/bbolt v1.3.10 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.uber.org/multierr v1.10.0 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.6.0 // indirect - google.golang.org/protobuf v1.35.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/blake3 v1.1.6 // indirect - modernc.org/libc v1.22.4 // indirect - modernc.org/mathutil v1.5.0 // indirect - modernc.org/memory v1.5.0 // indirect - modernc.org/sqlite v1.21.2 // indirect - zombiezen.com/go/sqlite v0.13.1 // indirect -) diff --git a/daemons/atorrent/go.sum b/daemons/atorrent/go.sum deleted file mode 100644 index 2fcf4f1..0000000 --- a/daemons/atorrent/go.sum +++ /dev/null @@ -1,629 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= -crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= -filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= -filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= -git.kmsign.ru/royalcat/tstor v0.0.0-20250120032914-a43371d1e3b9 h1:DHcbILTa1w3TWerW1Wkty/2TIsjK7Hr0yeoTBCYrf4U= -git.kmsign.ru/royalcat/tstor v0.0.0-20250120032914-a43371d1e3b9/go.mod h1:vhoCnS6pDoj3OS9jnFjdRPldAkcvxirfKh0aXxa88EI= -github.com/99designs/gqlgen v0.17.55 h1:3vzrNWYyzSZjGDFo68e5j9sSauLxfKvLp+6ioRokVtM= -github.com/99designs/gqlgen v0.17.55/go.mod h1:3Bq768f8hgVPGZxL8aY9MaYmbxa6llPM/qu1IGH1EJo= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= -github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= -github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= -github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY= -github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= -github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= -github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0= -github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k= -github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= -github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= -github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8= -github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI= -github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= -github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/anacrolix/chansync v0.4.1-0.20240627045151-1aa1ac392fe8 h1:eyb0bBaQKMOh5Se/Qg54shijc8K4zpQiOjEhKFADkQM= -github.com/anacrolix/chansync v0.4.1-0.20240627045151-1aa1ac392fe8/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k= -github.com/anacrolix/dht/v2 v2.22.0 h1:wat5FLdT25vltHsjX377GBrpK9o6L2QVn541bIguCYo= -github.com/anacrolix/dht/v2 v2.22.0/go.mod h1:shbBjhgvezqsJoE+hMo/ezHYQFF18V9jUllNIP5xV9k= -github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= -github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= -github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= -github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk= -github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0= -github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8= -github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca h1:aiiGqSQWjtVNdi8zUMfA//IrM8fPkv2bWwZVPbDe0wg= -github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca/go.mod h1:MN3ve08Z3zSV/rTuX/ouI4lNdlfTxgdafQJiLzyNRB8= -github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0= -github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o= -github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= -github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= -github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68= -github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY= -github.com/anacrolix/log v0.16.0 h1:DSuyb5kAJwl3Y0X1TRcStVrTS9ST9b0BHW+7neE4Xho= -github.com/anacrolix/log v0.16.0/go.mod h1:m0poRtlr41mriZlXBQ9SOVZ8yZBkLjOkDhd5Li5pITA= -github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM= -github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM= -github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s= -github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= -github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= -github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y= -github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw= -github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc= -github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy6BqESAJVw= -github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ= -github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY= -github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA= -github.com/anacrolix/missinggo/v2 v2.7.4 h1:47h5OXoPV8JbA/ACA+FLwKdYbAinuDO8osc2Cu9xkxg= -github.com/anacrolix/missinggo/v2 v2.7.4/go.mod h1:vVO5FEziQm+NFmJesc7StpkquZk+WJFCaL0Wp//2sa0= -github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw= -github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg= -github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc= -github.com/anacrolix/multiless v0.4.0 h1:lqSszHkliMsZd2hsyrDvHOw4AbYWa+ijQ66LzbjqWjM= -github.com/anacrolix/multiless v0.4.0/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM= -github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg= -github.com/anacrolix/stm v0.4.1-0.20221221005312-96d17df0e496 h1:aMiRi2kOOd+nG64suAmFMVnNK2E6GsnLif7ia9tI3cA= -github.com/anacrolix/stm v0.4.1-0.20221221005312-96d17df0e496/go.mod h1:DBm8/1OXm4A4RZ6Xa9u/eOsjeAXCaoRYvd2JzlskXeM= -github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk= -github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g= -github.com/anacrolix/sync v0.5.1 h1:FbGju6GqSjzVoTgcXTUKkF041lnZkG5P0C3T5RL3SGc= -github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g= -github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= -github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= -github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= -github.com/anacrolix/torrent v1.58.1-0.20241228235504-75e6b6565845 h1:ZuYsqgbLCVJHHmYQKG6ImMtz+3hUOI1qvRJTuxTVEZY= -github.com/anacrolix/torrent v1.58.1-0.20241228235504-75e6b6565845/go.mod h1:n3SjHIE8oHXeH0Px0d5FXQ7cU4IgbEfTroen6B9KWJk= -github.com/anacrolix/upnp v0.1.4 h1:+2t2KA6QOhm/49zeNyeVwDu1ZYS9dB9wfxyVvh/wk7U= -github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic= -github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4= -github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= -github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= -github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d h1:2qVb9bsAMtmAfnxXltm+6eBzrrS7SZ52c3SedsulaMI= -github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk= -github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= -github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= -github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= -github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/badger/v4 v4.5.0 h1:TeJE3I1pIWLBjYhIYCA1+uxrjWEoJXImFBMEBVSm16g= -github.com/dgraph-io/badger/v4 v4.5.0/go.mod h1:ysgYmIeG8dS/E8kwxT7xHyc7MkmwNYLRoYnFbr7387A= -github.com/dgraph-io/ristretto/v2 v2.0.0 h1:l0yiSOtlJvc0otkqyMaDNysg8E9/F/TYZwMbxscNOAQ= -github.com/dgraph-io/ristretto/v2 v2.0.0/go.mod h1:FVFokF2dRqXyPyeMnK1YDy8Fc6aTe0IKgbcd03CYeEk= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= -github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= -github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= -github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= -github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= -github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= -github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= -github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= -github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= -github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= -github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 h1:OyQmpAN302wAopDgwVjgs2HkFawP9ahIEqkUYz7V7CA= -github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU= -github.com/go-llsqlite/crawshaw v0.5.2-0.20240425034140-f30eb7704568 h1:3EpZo8LxIzF4q3BT+vttQQlRfA6uTtTb/cxVisWa5HM= -github.com/go-llsqlite/crawshaw v0.5.2-0.20240425034140-f30eb7704568/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid/v5 v5.1.0 h1:S5rqVKIigghZTCBKPCw0Y+bXkn26K3TB5mvQq2Ix8dk= -github.com/gofrs/uuid/v5 v5.1.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= -github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= -github.com/goware/singleflight v0.2.0 h1:e/hZsvNmbLoiZLx3XbihH01oXYA2MwLFo4e+N017U4c= -github.com/goware/singleflight v0.2.0/go.mod h1:SsAslCMS7HizXdbYcBQRBLC7HcNmFrHutRt3Hz6wovY= -github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= -github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= -github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= -github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= -github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= -github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= -github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= -github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg= -github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ= -github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c= -github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA= -github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0= -github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE= -github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= -github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= -github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= -github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= -github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= -github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY= -github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= -github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= -github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM= -github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU= -github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s= -github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg= -github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= -github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= -github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= -github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= -github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= -github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= -github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= -github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= -github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= -github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= -github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= -github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= -github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= -github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= -github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= -github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= -github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= -github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= -github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8= -github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE= -github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM= -github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 h1:UVArwN/wkKjMVhh2EQGC0tEc1+FqiLlvYXY5mQ2f8Wg= -github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/royalcat/btrgo v0.0.0-20240318160410-19bd27154450 h1:AZyZxXZLniAR0DaZhTS4RVcHtOvYMW8IunplqC9A0mk= -github.com/royalcat/btrgo v0.0.0-20240318160410-19bd27154450/go.mod h1:m3TPa9l/wMKpm/7WHrMs3dSFUxo7kLHaI8ap+SFGYhQ= -github.com/royalcat/ctxio v0.0.0-20240602084623-009bd79b3176 h1:2jCQJow6jRvhpdMJCo1Okd7tq5Rg4YXlUxqT0q0NWAg= -github.com/royalcat/ctxio v0.0.0-20240602084623-009bd79b3176/go.mod h1:81eB8eOH/UU7pzI7J1Rsg3KLpshF7BXg4+UHbex+27I= -github.com/royalcat/kv v0.0.0-20240723215915-954e36a2491d h1:MmqWGKR/MQdRBRieWVQXkn6X5dVyvwC/1H1WZEVLj3A= -github.com/royalcat/kv v0.0.0-20240723215915-954e36a2491d/go.mod h1:UMD8Uk5ph+34lFjD7WrEUdiivC3pyd1tTAGYv3+iukg= -github.com/royalcat/kv/kvbadger v0.0.0-20240723215915-954e36a2491d h1:87jn2CTjcxuLmQ2+8128bQD9m4qUM2F8oLNX+HvPGfc= -github.com/royalcat/kv/kvbadger v0.0.0-20240723215915-954e36a2491d/go.mod h1:ZvAOAmLJhB9YaaDZ8e0RDdbjOA4bHD7MilyMjongmS8= -github.com/royalcat/kv/testsuite v0.0.0-20240723124828-253d2ecf5312 h1:HJa7itFbdRh1S1MzLGLT56C7glaCOFC/zFnIIJleWVc= -github.com/royalcat/kv/testsuite v0.0.0-20240723124828-253d2ecf5312/go.mod h1:mnIN/3t3O7piZJW5N0Em79rO27EbupBcHZncbMeBwjE= -github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs= -github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= -github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= -github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= -github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= -github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= -github.com/samber/slog-multi v1.0.2 h1:6BVH9uHGAsiGkbbtQgAOQJMpKgV8unMrHhhJaw+X1EQ= -github.com/samber/slog-multi v1.0.2/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo= -github.com/samber/slog-zerolog v1.0.0 h1:YpRy0xux1uJr0Ng3wrEjv9nyvb4RAoNqkS611UjzeG8= -github.com/samber/slog-zerolog v1.0.0/go.mod h1:N2/g/mNGRY1zqsydIYE0uKipSSFsPDjytoVkRnZ0Jp0= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= -github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= -github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= -github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= -github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= -github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= -github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= -github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= -github.com/vektah/gqlparser/v2 v2.5.17 h1:9At7WblLV7/36nulgekUgIaqHZWn5hxqluxrxGUhOmI= -github.com/vektah/gqlparser/v2 v2.5.17/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= -github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 h1:U0DnHRZFzoIV1oFEZczg5XyPut9yxk9jjtax/9Bxr/o= -github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00/go.mod h1:Tq++Lr/FgiS3X48q5FETemXiSLGuYMQT2sPjYNPJSwA= -github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg= -github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= -go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= -lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= -modernc.org/libc v1.22.4 h1:wymSbZb0AlrjdAVX3cjreCHTPCpPARbQXNz6BHPzdwQ= -modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= -modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= -modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/sqlite v1.21.2 h1:ixuUG0QS413Vfzyx6FWx6PYTmHaOegTY+hjzhn7L+a0= -modernc.org/sqlite v1.21.2/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0= -zombiezen.com/go/sqlite v0.13.1 h1:qDzxyWWmMtSSEH5qxamqBFmqA2BLSSbtODi3ojaE02o= -zombiezen.com/go/sqlite v0.13.1/go.mod h1:Ht/5Rg3Ae2hoyh1I7gbWtWAl89CNocfqeb/aAMTkJr4= diff --git a/daemons/atorrent/graphql/mutation.graphql b/daemons/atorrent/graphql/mutation.graphql deleted file mode 100644 index 13ada15..0000000 --- a/daemons/atorrent/graphql/mutation.graphql +++ /dev/null @@ -1,18 +0,0 @@ -type TorrentDaemonMutation { - validateTorrent(filter: TorrentFilter!): Boolean! @resolver - setTorrentPriority( - infohash: String! - file: String - priority: TorrentPriority! - ): Boolean! @resolver - cleanup(files: Boolean, dryRun: Boolean!): CleanupResponse! @resolver -} - -type CleanupResponse { - count: Int! - list: [String!]! -} - -type DownloadTorrentResponse { - taskID: String! -} diff --git a/daemons/atorrent/graphql/query.graphql b/daemons/atorrent/graphql/query.graphql deleted file mode 100644 index 6ecd144..0000000 --- a/daemons/atorrent/graphql/query.graphql +++ /dev/null @@ -1,29 +0,0 @@ -type TorrentDaemonQuery { - torrents(filter: TorrentsFilter): [Torrent!]! @resolver - clientStats: TorrentClientStats! @resolver - statsHistory(since: DateTime!, infohash: String): [TorrentStats!]! @resolver -} - -input TorrentsFilter { - infohash: StringFilter - name: StringFilter - bytesCompleted: IntFilter - bytesMissing: IntFilter - peersCount: IntFilter - priority: TorrentPriorityFilter -} - -input TorrentPriorityFilter @oneOf { - eq: TorrentPriority - gt: TorrentPriority - lt: TorrentPriority - gte: TorrentPriority - lte: TorrentPriority - in: [TorrentPriority!] -} - -input TorrentFilter @oneOf { - everything: Boolean - infohash: String - # pathGlob: String! -} diff --git a/daemons/atorrent/graphql/schema.graphql b/daemons/atorrent/graphql/schema.graphql deleted file mode 100644 index 2284bb9..0000000 --- a/daemons/atorrent/graphql/schema.graphql +++ /dev/null @@ -1,21 +0,0 @@ -directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION -directive @resolver on INPUT_FIELD_DEFINITION | FIELD_DEFINITION - -directive @stream on FIELD_DEFINITION - -scalar DateTime -scalar Upload -scalar UInt - -type Schema { - query: Query - mutation: Mutation -} - -type Query { - torrentDaemon: TorrentDaemonQuery @resolver -} - -type Mutation { - torrentDaemon: TorrentDaemonMutation @resolver -} diff --git a/daemons/atorrent/graphql/types.graphql b/daemons/atorrent/graphql/types.graphql deleted file mode 100644 index ed47cb7..0000000 --- a/daemons/atorrent/graphql/types.graphql +++ /dev/null @@ -1,80 +0,0 @@ -type TorrentFS implements Dir & FsEntry { - name: String! - torrent: Torrent! - entries: [FsEntry!]! @resolver -} - -type TorrentFileEntry implements File & FsEntry { - name: String! - torrent: Torrent! - size: Int! -} - -type TorrentProgress implements Progress { - torrent: Torrent! - current: Int! - total: Int! -} - -type Torrent { - name: String! @resolver - infohash: String! - bytesCompleted: Int! - torrentFilePath: String! - bytesMissing: Int! - priority: TorrentPriority! - files: [TorrentFile!]! @resolver - excludedFiles: [TorrentFile!]! @resolver - peers: [TorrentPeer!]! @resolver -} - -type TorrentFile { - filename: String! - size: Int! - bytesCompleted: Int! - priority: TorrentPriority! @resolver -} - -type TorrentPeer { - ip: String! - downloadRate: Float! - discovery: String! - port: Int! - clientName: String! -} - -enum TorrentPriority { - NONE - NORMAL - HIGH - READAHEAD - NOW -} - -type TorrentClientStats { - bytesWritten: Int! - bytesWrittenData: Int! - bytesRead: Int! - bytesReadData: Int! - bytesReadUsefulData: Int! - bytesReadUsefulIntendedData: Int! - - chunksWritten: Int! - chunksRead: Int! - chunksReadUseful: Int! - chunksReadWasted: Int! - - metadataChunksRead: Int! - - piecesDirtiedGood: Int! - piecesDirtiedBad: Int! -} - -type TorrentStats { - timestamp: DateTime! - downloadedBytes: UInt! - uploadedBytes: UInt! - totalPeers: UInt! - activePeers: UInt! - connectedSeeders: UInt! -} diff --git a/daemons/atorrent/infobytes.go b/daemons/atorrent/infobytes.go deleted file mode 100644 index fbcc8fe..0000000 --- a/daemons/atorrent/infobytes.go +++ /dev/null @@ -1,90 +0,0 @@ -package atorrent - -import ( - "bytes" - "errors" - "fmt" - "path/filepath" - - "git.kmsign.ru/royalcat/tstor/src/logwrap" - "github.com/anacrolix/torrent/metainfo" - "github.com/anacrolix/torrent/types/infohash" - "github.com/dgraph-io/badger/v4" -) - -var errNotFound = errors.New("not found") - -type infoBytesStore struct { - db *badger.DB -} - -func newInfoBytesStore(metaDir string) (*infoBytesStore, error) { - opts := badger. - DefaultOptions(filepath.Join(metaDir, "infobytes")). - WithLogger(logwrap.BadgerLogger("torrent-client", "infobytes")) - db, err := badger.Open(opts) - if err != nil { - return nil, err - } - return &infoBytesStore{db}, nil -} - -func (k *infoBytesStore) GetBytes(ih infohash.T) ([]byte, error) { - var data []byte - err := k.db.View(func(tx *badger.Txn) error { - item, err := tx.Get(ih.Bytes()) - if err != nil { - if err == badger.ErrKeyNotFound { - return errNotFound - } - - return fmt.Errorf("error getting value: %w", err) - } - - data, err = item.ValueCopy(data) - return err - }) - return data, err -} - -func (k *infoBytesStore) Get(ih infohash.T) (*metainfo.MetaInfo, error) { - data, err := k.GetBytes(ih) - if err != nil { - return nil, err - } - - return metainfo.Load(bytes.NewReader(data)) -} - -func (me *infoBytesStore) SetBytes(ih infohash.T, data []byte) error { - return me.db.Update(func(txn *badger.Txn) error { - item, err := txn.Get(ih.Bytes()) - if err != nil { - if err == badger.ErrKeyNotFound { - return txn.Set(ih.Bytes(), data) - } - return err - } - - return item.Value(func(val []byte) error { - if !bytes.Equal(val, data) { - return txn.Set(ih.Bytes(), data) - } - return nil - }) - }) -} - -func (me *infoBytesStore) Set(ih infohash.T, info metainfo.MetaInfo) error { - return me.SetBytes(ih, info.InfoBytes) -} - -func (k *infoBytesStore) Delete(ih infohash.T) error { - return k.db.Update(func(txn *badger.Txn) error { - return txn.Delete(ih.Bytes()) - }) -} - -func (me *infoBytesStore) Close() error { - return me.db.Close() -} diff --git a/daemons/atorrent/metrics.go b/daemons/atorrent/metrics.go deleted file mode 100644 index 4e1754e..0000000 --- a/daemons/atorrent/metrics.go +++ /dev/null @@ -1,69 +0,0 @@ -package atorrent - -import ( - "context" - "encoding/base64" - - "github.com/anacrolix/dht/v2" - "github.com/anacrolix/torrent" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric" -) - -func registerTorrentMetrics(client *torrent.Client) error { - meterTotalPeers, _ := meter.Int64ObservableGauge("torrent.peers.total") - meterActivePeers, _ := meter.Int64ObservableGauge("torrent.peers.active") - meterSeeders, _ := meter.Int64ObservableGauge("torrent.seeders") - meterDownloaded, _ := meter.Int64ObservableGauge("torrent.downloaded", metric.WithUnit("By")) - meterIO, _ := meter.Int64ObservableGauge("torrent.io", metric.WithUnit("By")) - meterLoaded, _ := meter.Int64ObservableGauge("torrent.loaded") - - _, err := meter.RegisterCallback(func(ctx context.Context, o metric.Observer) error { - o.ObserveInt64(meterLoaded, int64(len(client.Torrents()))) - - for _, v := range client.Torrents() { - as := attribute.NewSet( - attribute.String("infohash", v.InfoHash().HexString()), - attribute.String("name", v.Name()), - attribute.Int64("size", v.Length()), - ) - stats := v.Stats() - o.ObserveInt64(meterTotalPeers, int64(stats.TotalPeers), metric.WithAttributeSet(as)) - o.ObserveInt64(meterActivePeers, int64(stats.ActivePeers), metric.WithAttributeSet(as)) - o.ObserveInt64(meterSeeders, int64(stats.ConnectedSeeders), metric.WithAttributeSet(as)) - o.ObserveInt64(meterIO, stats.BytesRead.Int64(), metric.WithAttributeSet(as), metric.WithAttributes(attribute.String("direction", "download"))) - o.ObserveInt64(meterIO, stats.BytesWritten.Int64(), metric.WithAttributeSet(as), metric.WithAttributes(attribute.String("direction", "upload"))) - o.ObserveInt64(meterDownloaded, v.BytesCompleted(), metric.WithAttributeSet(as)) - } - - return nil - }, meterTotalPeers, meterActivePeers, meterSeeders, meterIO, meterDownloaded, meterLoaded) - if err != nil { - return err - } - return nil -} - -func registerDhtMetrics(client *torrent.Client) error { - meterDhtNodes, _ := meter.Int64ObservableGauge("torrent.dht.nodes") - - _, err := meter.RegisterCallback(func(ctx context.Context, o metric.Observer) error { - servers := client.DhtServers() - for _, dhtSrv := range servers { - stats, ok := dhtSrv.Stats().(dht.ServerStats) - if !ok { - continue - } - id := dhtSrv.ID() - as := attribute.NewSet( - attribute.String("id", base64.StdEncoding.EncodeToString(id[:])), - attribute.String("address", dhtSrv.Addr().String()), - ) - o.ObserveInt64(meterDhtNodes, int64(stats.Nodes), metric.WithAttributeSet(as)) - } - - return nil - }, meterDhtNodes) - - return err -} diff --git a/daemons/atorrent/peer_store.go b/daemons/atorrent/peer_store.go deleted file mode 100644 index 6f030e3..0000000 --- a/daemons/atorrent/peer_store.go +++ /dev/null @@ -1,24 +0,0 @@ -package atorrent - -import ( - "github.com/anacrolix/dht/v2/krpc" - peer_store "github.com/anacrolix/dht/v2/peer-store" - "github.com/anacrolix/torrent/types/infohash" - "github.com/royalcat/kv" -) - -type peerStore struct { - store kv.Store[infohash.T, []krpc.NodeAddr] -} - -var _ peer_store.Interface = (*peerStore)(nil) - -// AddPeer implements peer_store.Interface. -func (p *peerStore) AddPeer(ih infohash.T, node krpc.NodeAddr) { - panic("unimplemented") -} - -// GetPeers implements peer_store.Interface. -func (p *peerStore) GetPeers(ih infohash.T) []krpc.NodeAddr { - panic("unimplemented") -} diff --git a/daemons/atorrent/piece_completion.go b/daemons/atorrent/piece_completion.go deleted file mode 100644 index 013a0bb..0000000 --- a/daemons/atorrent/piece_completion.go +++ /dev/null @@ -1,137 +0,0 @@ -package atorrent - -import ( - "context" - "encoding/binary" - "fmt" - - "git.kmsign.ru/royalcat/tstor/src/logwrap" - "github.com/anacrolix/torrent/metainfo" - "github.com/anacrolix/torrent/storage" - "github.com/royalcat/kv" - "github.com/royalcat/kv/kvbadger" -) - -type PieceCompletionState byte - -const ( - PieceNotComplete PieceCompletionState = 0 - PieceComplete PieceCompletionState = 1<<8 - 1 -) - -var _ kv.Binary = (*PieceCompletionState)(nil) - -// MarshalBinary implements kv.Binary. -func (p PieceCompletionState) MarshalBinary() (data []byte, err error) { - return []byte{byte(p)}, nil -} - -// UnmarshalBinary implements kv.Binary. -func (p *PieceCompletionState) UnmarshalBinary(data []byte) error { - if len(data) != 1 { - return fmt.Errorf("bad length") - } - - switch PieceCompletionState(data[0]) { - case PieceComplete: - *p = PieceComplete - case PieceNotComplete: - *p = PieceNotComplete - default: - *p = PieceNotComplete - } - - return nil -} - -func pieceCompletionState(i bool) PieceCompletionState { - if i { - return PieceComplete - } - return PieceNotComplete -} - -type pieceKey metainfo.PieceKey - -const pieceKeySize = metainfo.HashSize + 4 - -var _ kv.Binary = (*pieceKey)(nil) - -// const delimeter rune = 0x1F - -// MarshalBinary implements kv.Binary. -func (pk pieceKey) MarshalBinary() (data []byte, err error) { - key := make([]byte, 0, pieceKeySize) - key = append(key, pk.InfoHash.Bytes()...) - key = binary.BigEndian.AppendUint32(key, uint32(pk.Index)) - return key, nil -} - -// UnmarshalBinary implements kv.Binary. -func (p *pieceKey) UnmarshalBinary(data []byte) error { - if len(data) < pieceKeySize { - return fmt.Errorf("data too short") - } - p.InfoHash = metainfo.Hash(data[:metainfo.HashSize]) - p.Index = int(binary.BigEndian.Uint32(data[metainfo.HashSize:])) - return nil -} - -type badgerPieceCompletion struct { - db kv.Store[pieceKey, PieceCompletionState] -} - -var _ storage.PieceCompletion = (*badgerPieceCompletion)(nil) - -func newPieceCompletion(dir string) (storage.PieceCompletion, error) { - opts := kvbadger.DefaultOptions[PieceCompletionState](dir) - opts.Codec = kv.CodecBinary[PieceCompletionState, *PieceCompletionState]{} - opts.BadgerOptions = opts.BadgerOptions.WithLogger(logwrap.BadgerLogger("torrent-client", "piece-completion")) - - db, err := kvbadger.NewBinaryKey[pieceKey, PieceCompletionState](opts) - if err != nil { - return nil, err - } - - return &badgerPieceCompletion{ - db: db, - }, nil -} - -func (c *badgerPieceCompletion) Get(pk metainfo.PieceKey) (completion storage.Completion, err error) { - ctx := context.Background() - - state, err := c.db.Get(ctx, pieceKey(pk)) - if err != nil { - if err == kv.ErrKeyNotFound { - return completion, nil - } - return completion, err - } - - if state == PieceComplete { - return storage.Completion{ - Complete: true, - Ok: true, - }, nil - } - - return storage.Completion{ - Complete: false, - Ok: true, - }, nil -} - -func (me badgerPieceCompletion) Set(pk metainfo.PieceKey, b bool) error { - ctx := context.Background() - - if c, err := me.Get(pk); err == nil && c.Ok && c.Complete == b { - return nil - } - - return me.db.Set(ctx, pieceKey(pk), pieceCompletionState(b)) -} - -func (me *badgerPieceCompletion) Close() error { - return me.db.Close(context.Background()) -} diff --git a/daemons/atorrent/piece_completion_test.go b/daemons/atorrent/piece_completion_test.go deleted file mode 100644 index c6acf0e..0000000 --- a/daemons/atorrent/piece_completion_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package atorrent - -import ( - "testing" - - "github.com/anacrolix/torrent/metainfo" - "github.com/anacrolix/torrent/storage" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBoltPieceCompletion(t *testing.T) { - td := t.TempDir() - - pc, err := newPieceCompletion(td) - require.NoError(t, err) - defer pc.Close() - - pk := metainfo.PieceKey{} - - b, err := pc.Get(pk) - require.NoError(t, err) - assert.False(t, b.Ok) - - require.NoError(t, pc.Set(pk, false)) - - b, err = pc.Get(pk) - require.NoError(t, err) - assert.Equal(t, storage.Completion{Complete: false, Ok: true}, b) - - require.NoError(t, pc.Set(pk, true)) - - b, err = pc.Get(pk) - require.NoError(t, err) - assert.Equal(t, storage.Completion{Complete: true, Ok: true}, b) -} diff --git a/daemons/atorrent/queue.go b/daemons/atorrent/queue.go deleted file mode 100644 index 2fcd004..0000000 --- a/daemons/atorrent/queue.go +++ /dev/null @@ -1,135 +0,0 @@ -package atorrent - -import ( - "context" - "fmt" - - "git.kmsign.ru/royalcat/tstor/pkg/uuid" - "github.com/anacrolix/torrent" - "github.com/anacrolix/torrent/types/infohash" -) - -type DownloadTask struct { - ID uuid.UUID - InfoHash infohash.T - File string -} - -func (s *Daemon) Download(ctx context.Context, task *DownloadTask) error { - t, ok := s.client.Torrent(task.InfoHash) - if !ok { - return fmt.Errorf("torrent with IH %s not found", task.InfoHash.HexString()) - } - - if task.File != "" { - var file *torrent.File - for _, tf := range t.Files() { - if tf.Path() == task.File { - file = tf - break - } - } - - if file == nil { - return fmt.Errorf("file %s not found in torrent torrent with IH %s", task.File, task.InfoHash.HexString()) - } - - file.Download() - } else { - for _, file := range t.Files() { - file.Download() - } - } - - return nil -} - -// func (s *Service) DownloadAndWait(ctx context.Context, task *TorrentDownloadTask) error { -// t, ok := s.c.Torrent(task.InfoHash) -// if !ok { -// return fmt.Errorf("torrent with IH %s not found", task.InfoHash.HexString()) -// } - -// if task.File != "" { -// var file *torrent.File -// for _, tf := range t.Files() { -// if tf.Path() == task.File { -// file = tf -// break -// } -// } - -// if file == nil { -// return fmt.Errorf("file %s not found in torrent torrent with IH %s", task.File, task.InfoHash.HexString()) -// } - -// file.Download() -// return waitPieceRange(ctx, t, file.BeginPieceIndex(), file.EndPieceIndex()) - -// } - -// t.DownloadAll() -// select { -// case <-ctx.Done(): -// return ctx.Err() -// case <-t.Complete.On(): -// return nil -// } -// } - -// func waitPieceRange(ctx context.Context, t *torrent.Torrent, start, end int) error { -// for i := start; i < end; i++ { -// timer := time.NewTimer(time.Millisecond) -// for { -// select { -// case <-ctx.Done(): -// return ctx.Err() -// case <-timer.C: -// if t.PieceState(i).Complete { -// continue -// } -// } - -// } -// } -// return nil -// } - -type TorrentProgress struct { - Torrent *Controller - Current int64 - Total int64 -} - -func (s *Daemon) DownloadProgress(ctx context.Context) (<-chan TorrentProgress, error) { - torrents, err := s.ListTorrents(ctx) - if err != nil { - return nil, err - } - - out := make(chan TorrentProgress, 1) - go func() { - defer close(out) - for _, t := range torrents { - sub := t.Torrent().SubscribePieceStateChanges() - go func(t *Controller) { - for stateChange := range sub.Values { - if !stateChange.Complete && !stateChange.Partial { - continue - } - - out <- TorrentProgress{ - Torrent: t, - Current: t.BytesCompleted(), - Total: t.Length(), - } - } - }(t) - defer sub.Close() - } - - <-ctx.Done() - }() - - return out, nil -} diff --git a/daemons/atorrent/setup.go b/daemons/atorrent/setup.go deleted file mode 100644 index 04ff54d..0000000 --- a/daemons/atorrent/setup.go +++ /dev/null @@ -1,56 +0,0 @@ -package atorrent - -import ( - "fmt" - "os" - "path/filepath" - - "git.kmsign.ru/royalcat/tstor/src/config" - "github.com/anacrolix/torrent/storage" -) - -func setupStorage(cfg config.TorrentClient) (*fileStorage, storage.PieceCompletion, error) { - pcp := filepath.Join(cfg.MetadataFolder, "piece-completion") - if err := os.MkdirAll(pcp, 0744); err != nil { - return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err) - } - - // pc, err := storage.NewBoltPieceCompletion(pcp) - // if err != nil { - // return nil, nil, err - // } - - pc, err := newPieceCompletion(pcp) - if err != nil { - return nil, nil, fmt.Errorf("error creating servers piece completion: %w", err) - } - - // TODO implement cache/storage switching - // cacheDir := filepath.Join(tcfg.DataFolder, "cache") - // if err := os.MkdirAll(cacheDir, 0744); err != nil { - // return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err) - // } - // fc, err := filecache.NewCache(cacheDir) - // if err != nil { - // return nil, nil, fmt.Errorf("error creating cache: %w", err) - // } - // log.Info().Msg(fmt.Sprintf("setting cache size to %d MB", 1024)) - // fc.SetCapacity(1024 * 1024 * 1024) - - // rp := storage.NewResourcePieces(fc.AsResourceProvider()) - // st := &stc{rp} - - filesDir := cfg.DataFolder - if err := os.MkdirAll(filesDir, 0744); err != nil { - return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err) - } - st := NewFileStorage(filesDir, pc) - - // piecesDir := filepath.Join(cfg.DataFolder, ".pieces") - // if err := os.MkdirAll(piecesDir, 0744); err != nil { - // return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err) - // } - // st := storage.NewMMapWithCompletion(piecesDir, pc) - - return st, pc, nil -} diff --git a/daemons/atorrent/stats.go b/daemons/atorrent/stats.go deleted file mode 100644 index 2451d82..0000000 --- a/daemons/atorrent/stats.go +++ /dev/null @@ -1,207 +0,0 @@ -package atorrent - -import ( - "context" - "encoding/json" - "path" - "slices" - "time" - - "git.kmsign.ru/royalcat/tstor/src/logwrap" - "github.com/anacrolix/torrent/types/infohash" - "github.com/dgraph-io/badger/v4" -) - -func newStatsStore(metaDir string, lifetime time.Duration) (*statsStore, error) { - db, err := badger.OpenManaged( - badger. - DefaultOptions(path.Join(metaDir, "stats")). - WithNumVersionsToKeep(int(^uint(0) >> 1)). - WithLogger(logwrap.BadgerLogger("stats")), // Infinity - ) - if err != nil { - return nil, err - } - - go func() { - for n := range time.NewTimer(lifetime / 2).C { - db.SetDiscardTs(uint64(n.Add(-lifetime).Unix())) - } - }() - return &statsStore{ - db: db, - }, nil -} - -type statsStore struct { - db *badger.DB -} - -type TorrentStats struct { - Timestamp time.Time - DownloadedBytes uint64 - UploadedBytes uint64 - TotalPeers uint16 - ActivePeers uint16 - ConnectedSeeders uint16 -} - -func (s TorrentStats) Same(o TorrentStats) bool { - return s.DownloadedBytes == o.DownloadedBytes && - s.UploadedBytes == o.UploadedBytes && - s.TotalPeers == o.TotalPeers && - s.ActivePeers == o.ActivePeers && - s.ConnectedSeeders == o.ConnectedSeeders -} - -func (r *statsStore) addStats(key []byte, stat TorrentStats) error { - ts := uint64(stat.Timestamp.Unix()) - - txn := r.db.NewTransactionAt(ts, true) - defer txn.Discard() - - item, err := txn.Get(key) - if err != nil && err != badger.ErrKeyNotFound { - return err - } - - if err != badger.ErrKeyNotFound { - var prevStats TorrentStats - err = item.Value(func(val []byte) error { - return json.Unmarshal(val, &prevStats) - }) - if err != nil { - return err - } - - if prevStats.Same(stat) { - return nil - } - } - - data, err := json.Marshal(stat) - if err != nil { - return err - } - err = txn.Set(key, data) - if err != nil { - return err - } - - return txn.CommitAt(ts, nil) -} - -func (r *statsStore) AddTorrentStats(ih infohash.T, stat TorrentStats) error { - return r.addStats(ih.Bytes(), stat) -} - -const totalKey = "total" - -func (r *statsStore) AddTotalStats(stat TorrentStats) error { - return r.addStats([]byte(totalKey), stat) -} - -func (r *statsStore) ReadTotalStatsHistory(ctx context.Context, since time.Time) ([]TorrentStats, error) { - stats := []TorrentStats{} - - err := r.db.View(func(txn *badger.Txn) error { - opts := badger.DefaultIteratorOptions - opts.AllVersions = true - opts.SinceTs = uint64(since.Unix()) - - it := txn.NewKeyIterator([]byte(totalKey), opts) - defer it.Close() - for it.Rewind(); it.Valid(); it.Next() { - item := it.Item() - var stat TorrentStats - err := item.Value(func(v []byte) error { - return json.Unmarshal(v, &stat) - }) - if err != nil { - return err - } - - stats = append(stats, stat) - } - return nil - }) - if err != nil { - return nil, err - } - - slices.SortFunc(stats, func(a, b TorrentStats) int { - return a.Timestamp.Compare(b.Timestamp) - }) - stats = slices.Compact(stats) - return stats, nil -} - -func (r *statsStore) ReadTorrentStatsHistory(ctx context.Context, since time.Time, ih infohash.T) ([]TorrentStats, error) { - stats := []TorrentStats{} - - err := r.db.View(func(txn *badger.Txn) error { - opts := badger.DefaultIteratorOptions - opts.AllVersions = true - opts.SinceTs = uint64(since.Unix()) - - it := txn.NewKeyIterator(ih.Bytes(), opts) - defer it.Close() - for it.Rewind(); it.Valid(); it.Next() { - item := it.Item() - var stat TorrentStats - err := item.Value(func(v []byte) error { - return json.Unmarshal(v, &stat) - }) - if err != nil { - return err - } - - stats = append(stats, stat) - } - return nil - }) - if err != nil { - return nil, err - } - - slices.SortFunc(stats, func(a, b TorrentStats) int { - return a.Timestamp.Compare(b.Timestamp) - }) - stats = slices.Compact(stats) - return stats, nil -} - -func (r *statsStore) ReadStatsHistory(ctx context.Context, since time.Time) ([]TorrentStats, error) { - stats := []TorrentStats{} - - err := r.db.View(func(txn *badger.Txn) error { - opts := badger.DefaultIteratorOptions - opts.AllVersions = true - opts.SinceTs = uint64(since.Unix()) - - it := txn.NewIterator(opts) - defer it.Close() - for it.Rewind(); it.Valid(); it.Next() { - item := it.Item() - var stat TorrentStats - err := item.Value(func(v []byte) error { - return json.Unmarshal(v, &stat) - }) - if err != nil { - return err - } - - stats = append(stats, stat) - } - return nil - }) - if err != nil { - return nil, err - } - - slices.SortFunc(stats, func(a, b TorrentStats) int { - return a.Timestamp.Compare(b.Timestamp) - }) - stats = slices.Compact(stats) - return stats, nil -} diff --git a/daemons/atorrent/storage.go b/daemons/atorrent/storage.go deleted file mode 100644 index c805c20..0000000 --- a/daemons/atorrent/storage.go +++ /dev/null @@ -1,196 +0,0 @@ -package atorrent - -import ( - "context" - "errors" - "io/fs" - "log/slog" - "os" - "path" - "path/filepath" - "slices" - - "git.kmsign.ru/royalcat/tstor/pkg/rlog" - "github.com/anacrolix/torrent" - "github.com/anacrolix/torrent/metainfo" - "github.com/anacrolix/torrent/storage" -) - -// NewFileStorage creates a new ClientImplCloser that stores files using the OS native filesystem. -func NewFileStorage(baseDir string, pc storage.PieceCompletion) *fileStorage { - return &fileStorage{ - client: storage.NewFileOpts(storage.NewFileClientOpts{ - ClientBaseDir: baseDir, - PieceCompletion: pc, - TorrentDirMaker: func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string { - return torrentDir(baseDir, infoHash) - }, - FilePathMaker: func(opts storage.FilePathMakerOpts) string { - return filePath(*opts.File) - }, - }), - baseDir: baseDir, - pieceCompletion: pc, - dupIndex: newDupIndex(), - log: rlog.Component("daemon", "torrent"), - } -} - -// File-based storage for torrents, that isn't yet bound to a particular torrent. -type fileStorage struct { - baseDir string - client storage.ClientImplCloser - pieceCompletion storage.PieceCompletion - dupIndex *dupIndex - log *rlog.Logger -} - -var _ storage.ClientImplCloser = (*fileStorage)(nil) - -func (me *fileStorage) Close() error { - return errors.Join( - me.client.Close(), - me.pieceCompletion.Close(), - ) -} - -func (fs *fileStorage) fullFilePath(infoHash metainfo.Hash, fileInfo metainfo.FileInfo) string { - return filepath.Join( - torrentDir(fs.baseDir, infoHash), - filePath(fileInfo), - ) -} - -func (fs *fileStorage) DeleteFile(file *torrent.File) error { - infoHash := file.Torrent().InfoHash() - torrentDir := torrentDir(fs.baseDir, infoHash) - fileInfo := file.FileInfo() - relFilePath := filePath(fileInfo) - filePath := path.Join(torrentDir, relFilePath) - for i := file.BeginPieceIndex(); i < file.EndPieceIndex(); i++ { - pk := metainfo.PieceKey{InfoHash: infoHash, Index: i} - err := fs.pieceCompletion.Set(pk, false) - if err != nil { - return err - } - } - return os.Remove(filePath) -} - -func (fs *fileStorage) CleanupDirs(ctx context.Context, expected []*Controller, dryRun bool) ([]string, error) { - log := fs.log.With(slog.Int("expectedTorrents", len(expected)), slog.Bool("dryRun", dryRun)) - - expectedEntries := []string{} - for _, e := range expected { - expectedEntries = append(expectedEntries, e.Torrent().InfoHash().HexString()) - } - - entries, err := os.ReadDir(fs.baseDir) - if err != nil { - return nil, err - } - - toDelete := []string{} - for _, v := range entries { - if !slices.Contains(expectedEntries, v.Name()) { - toDelete = append(toDelete, v.Name()) - } - } - - if ctx.Err() != nil { - return nil, ctx.Err() - } - - log.Info(ctx, "deleting trash data", slog.Int("dirsCount", len(toDelete))) - if !dryRun { - for i, name := range toDelete { - p := path.Join(fs.baseDir, name) - log.Warn(ctx, "deleting trash data", slog.String("path", p)) - err := os.RemoveAll(p) - if err != nil { - return toDelete[:i], err - } - } - } - - return toDelete, nil -} - -func (s *fileStorage) CleanupFiles(ctx context.Context, expected []*Controller, dryRun bool) ([]string, error) { - log := s.log.With(slog.Int("expectedTorrents", len(expected)), slog.Bool("dryRun", dryRun)) - - expectedEntries := []string{} - { - for _, e := range expected { - files, err := e.Files(ctx) - if err != nil { - return nil, err - } - - for _, f := range files { - expectedEntries = append(expectedEntries, s.fullFilePath(e.Torrent().InfoHash(), f.FileInfo())) - } - } - } - - entries := []string{} - err := filepath.WalkDir(s.baseDir, - func(path string, info fs.DirEntry, err error) error { - if err != nil { - return err - } - if ctx.Err() != nil { - return ctx.Err() - } - - if info.IsDir() { - return nil - } - entries = append(entries, path) - return nil - }) - if err != nil { - return nil, err - } - - toDelete := []string{} - for _, v := range entries { - if !slices.Contains(expectedEntries, v) { - toDelete = append(toDelete, v) - } - } - - if ctx.Err() != nil { - return toDelete, ctx.Err() - } - - log.Info(ctx, "deleting trash data", slog.Int("filesCount", len(toDelete))) - if !dryRun { - for i, p := range toDelete { - s.log.Warn(ctx, "deleting trash data", slog.String("path", p)) - err := os.Remove(p) - if err != nil { - return toDelete[i:], err - } - } - } - return toDelete, nil -} - -func (s *fileStorage) iterFiles(ctx context.Context, iter func(ctx context.Context, path string, entry fs.FileInfo) error) error { - return filepath.Walk(s.baseDir, - func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - if ctx.Err() != nil { - return ctx.Err() - } - - if info.IsDir() { - return nil - } - - return iter(ctx, path, info) - }) -} diff --git a/daemons/atorrent/storage_dedupe.go b/daemons/atorrent/storage_dedupe.go deleted file mode 100644 index e8f91a1..0000000 --- a/daemons/atorrent/storage_dedupe.go +++ /dev/null @@ -1,225 +0,0 @@ -package atorrent - -import ( - "context" - "crypto/sha1" - "fmt" - "io" - "io/fs" - "log/slog" - "os" - "path/filepath" - "slices" - - "git.kmsign.ru/royalcat/tstor/pkg/rlog" - "github.com/dustin/go-humanize" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "golang.org/x/exp/maps" - "golang.org/x/sys/unix" -) - -func (s *fileStorage) Dedupe(ctx context.Context) (uint64, error) { - ctx, span := tracer.Start(ctx, fmt.Sprintf("Dedupe")) - defer span.End() - - log := s.log - - sizeMap := map[int64][]string{} - err := s.iterFiles(ctx, func(ctx context.Context, path string, info fs.FileInfo) error { - size := info.Size() - sizeMap[size] = append(sizeMap[size], path) - return nil - }) - if err != nil { - return 0, err - } - - maps.DeleteFunc(sizeMap, func(k int64, v []string) bool { - return len(v) <= 1 - }) - - span.AddEvent("collected files with same size", trace.WithAttributes( - attribute.Int("count", len(sizeMap)), - )) - - var deduped uint64 = 0 - - i := 0 - for _, paths := range sizeMap { - if i%100 == 0 { - log.Info(ctx, "deduping in progress", slog.Int("current", i), slog.Int("total", len(sizeMap))) - } - i++ - - if ctx.Err() != nil { - return deduped, ctx.Err() - } - - slices.Sort(paths) - paths = slices.Compact(paths) - if len(paths) <= 1 { - continue - } - - paths, err = applyErr(paths, filepath.Abs) - if err != nil { - return deduped, err - } - - dedupedGroup, err := s.dedupeFiles(ctx, paths) - if err != nil { - log.Error(ctx, "Error applying dedupe", slog.Any("files", paths), rlog.Error(err)) - continue - } - - if dedupedGroup > 0 { - deduped += dedupedGroup - log.Info(ctx, "deduped file group", - slog.String("files", fmt.Sprint(paths)), - slog.String("deduped", humanize.Bytes(dedupedGroup)), - slog.String("deduped_total", humanize.Bytes(deduped)), - ) - } - - } - - return deduped, nil -} - -func applyErr[E, O any](in []E, apply func(E) (O, error)) ([]O, error) { - out := make([]O, 0, len(in)) - for _, p := range in { - o, err := apply(p) - if err != nil { - return out, err - } - out = append(out, o) - - } - return out, nil -} - -// const blockSize uint64 = 4096 - -func (s *fileStorage) dedupeFiles(ctx context.Context, paths []string) (deduped uint64, err error) { - ctx, span := tracer.Start(ctx, fmt.Sprintf("dedupeFiles"), trace.WithAttributes( - attribute.StringSlice("files", paths), - )) - defer func() { - span.SetAttributes(attribute.Int64("deduped", int64(deduped))) - if err != nil { - span.RecordError(err) - } - span.End() - }() - - log := s.log - - srcF, err := os.Open(paths[0]) - if err != nil { - return deduped, fmt.Errorf("error opening file %s: %w", paths[0], err) - } - defer srcF.Close() - srcStat, err := srcF.Stat() - if err != nil { - return deduped, fmt.Errorf("error stat file %s: %w", paths[0], err) - } - - srcFd := int(srcF.Fd()) - srcSize := srcStat.Size() - - fsStat := unix.Statfs_t{} - err = unix.Fstatfs(srcFd, &fsStat) - if err != nil { - span.RecordError(err) - return deduped, fmt.Errorf("error statfs file %s: %w", paths[0], err) - } - - srcHash, err := filehash(srcF) - if err != nil { - return deduped, fmt.Errorf("error hashing file %s: %w", paths[0], err) - } - - if int64(fsStat.Bsize) > srcSize { // for btrfs it means file in residing in not deduplicatable metadata - return deduped, nil - } - - blockSize := uint64((srcSize % int64(fsStat.Bsize)) * int64(fsStat.Bsize)) - - span.SetAttributes(attribute.Int64("blocksize", int64(blockSize))) - - rng := unix.FileDedupeRange{ - Src_offset: 0, - Src_length: blockSize, - Info: []unix.FileDedupeRangeInfo{}, - } - - for _, dst := range paths[1:] { - if ctx.Err() != nil { - return deduped, ctx.Err() - } - - destF, err := os.OpenFile(dst, os.O_RDWR, os.ModePerm) - if err != nil { - return deduped, fmt.Errorf("error opening file %s: %w", dst, err) - } - defer destF.Close() - - dstHash, err := filehash(destF) - if err != nil { - return deduped, fmt.Errorf("error hashing file %s: %w", dst, err) - } - - if srcHash != dstHash { - destF.Close() - continue - } - - rng.Info = append(rng.Info, unix.FileDedupeRangeInfo{ - Dest_fd: int64(destF.Fd()), - Dest_offset: 0, - }) - } - - if len(rng.Info) == 0 { - return deduped, nil - } - - log.Info(ctx, "found same files, deduping", slog.Any("files", paths), slog.String("size", humanize.Bytes(uint64(srcStat.Size())))) - - if ctx.Err() != nil { - return deduped, ctx.Err() - } - - rng.Src_offset = 0 - for i := range rng.Info { - rng.Info[i].Dest_offset = 0 - } - - err = unix.IoctlFileDedupeRange(srcFd, &rng) - if err != nil { - return deduped, fmt.Errorf("error calling FIDEDUPERANGE: %w", err) - } - - for i := range rng.Info { - deduped += rng.Info[i].Bytes_deduped - - rng.Info[i].Status = 0 - rng.Info[i].Bytes_deduped = 0 - } - - return deduped, nil -} - -const compareBlockSize = 1024 * 128 - -func filehash(r io.Reader) ([20]byte, error) { - buf := make([]byte, compareBlockSize) - _, err := r.Read(buf) - if err != nil && err != io.EOF { - return [20]byte{}, err - } - - return sha1.Sum(buf), nil -} diff --git a/daemons/atorrent/storage_open.go b/daemons/atorrent/storage_open.go deleted file mode 100644 index 4c8eb66..0000000 --- a/daemons/atorrent/storage_open.go +++ /dev/null @@ -1,179 +0,0 @@ -package atorrent - -import ( - "context" - "errors" - "fmt" - "log/slog" - "os" - "path/filepath" - "slices" - "strings" - - "git.kmsign.ru/royalcat/tstor/pkg/cowutils" - "git.kmsign.ru/royalcat/tstor/pkg/rlog" - "github.com/anacrolix/torrent/metainfo" - "github.com/anacrolix/torrent/storage" - "github.com/anacrolix/torrent/types/infohash" - "github.com/royalcat/kv" -) - -// OpenTorrent implements storage.ClientImplCloser. -func (me *fileStorage) OpenTorrent(ctx context.Context, info *metainfo.Info, infoHash infohash.T) (storage.TorrentImpl, error) { - ctx, span := tracer.Start(ctx, "OpenTorrent") - defer span.End() - log := me.log.With(slog.String("infohash", infoHash.HexString()), slog.String("name", info.BestName())) - - log.Debug(ctx, "opening torrent") - - impl, err := me.client.OpenTorrent(ctx, info, infoHash) - if err != nil { - log.Error(ctx, "error opening torrent", rlog.Error(err)) - } - return impl, err -} - -func (me *fileStorage) copyDup(ctx context.Context, infoHash infohash.T, dup dupInfo) error { - log := me.log.With(slog.String("infohash", infoHash.HexString()), slog.String("dup_infohash", dup.infohash.HexString())) - - srcPath := me.fullFilePath(dup.infohash, dup.fileinfo) - src, err := os.Open(me.fullFilePath(dup.infohash, dup.fileinfo)) - if err != nil { - return err - } - - dstPath := me.fullFilePath(infoHash, dup.fileinfo) - dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) - if err != nil { - return err - } - - log.Info(ctx, "copying duplicate file", slog.String("src", srcPath), slog.String("dst", dstPath)) - - err = cowutils.Reflink(ctx, dst, src, true) - if err != nil { - return fmt.Errorf("error copying file: %w", err) - } - - return nil -} - -func torrentDir(baseDir string, infoHash metainfo.Hash) string { - return filepath.Join(baseDir, infoHash.HexString()) -} - -func filePath(file metainfo.FileInfo) string { - return filepath.Join(file.BestPath()...) -} - -func (s *Daemon) checkTorrentCompatable(ctx context.Context, ih infohash.T, info metainfo.Info) (compatable bool, tryLater bool, err error) { - log := s.log.With( - slog.String("new-name", info.BestName()), - slog.String("new-infohash", ih.String()), - ) - - name := info.BestName() - - aq, err := s.dirsAquire.Get(ctx, info.BestName()) - if errors.Is(err, kv.ErrKeyNotFound) { - err = s.dirsAquire.Set(ctx, name, DirAquire{ - Name: name, - Hashes: slices.Compact([]infohash.T{ih}), - }) - if err != nil { - return false, false, err - } - - log.Debug(ctx, "acquiring was not found, so created") - return true, false, nil - } else if err != nil { - return false, false, err - } - - if slices.Contains(aq.Hashes, ih) { - log.Debug(ctx, "hash already know to be compatable") - return true, false, nil - } - - for _, existingTorrent := range s.client.Torrents() { - if existingTorrent.Name() != name || existingTorrent.InfoHash() == ih { - continue - } - - existingInfo := existingTorrent.Info() - - existingFiles := slices.Clone(existingInfo.Files) - newFiles := slices.Clone(info.Files) - - if !s.checkTorrentFilesCompatable(ctx, aq, existingFiles, newFiles) { - return false, false, nil - } - - aq.Hashes = slicesUnique(append(aq.Hashes, ih)) - err = s.dirsAquire.Set(ctx, aq.Name, aq) - if err != nil { - log.Warn(ctx, "torrent not compatible") - return false, false, err - } - - } - if slices.Contains(aq.Hashes, ih) { - log.Debug(ctx, "hash is compatable") - return true, false, nil - } - - log.Debug(ctx, "torrent with same name not found, try later") - return false, true, nil -} - -func (s *Daemon) checkTorrentFilesCompatable(ctx context.Context, aq DirAquire, existingFiles, newFiles []metainfo.FileInfo) bool { - log := s.log.With(slog.String("name", aq.Name)) - - pathCmp := func(a, b metainfo.FileInfo) int { - return slices.Compare(a.BestPath(), b.BestPath()) - } - slices.SortStableFunc(existingFiles, pathCmp) - slices.SortStableFunc(newFiles, pathCmp) - - // torrents basically equals - if slices.EqualFunc(existingFiles, newFiles, func(fi1, fi2 metainfo.FileInfo) bool { - return fi1.Length == fi2.Length && slices.Equal(fi1.BestPath(), fi1.BestPath()) - }) { - return true - } - - if len(newFiles) > len(existingFiles) { - type fileInfo struct { - Path string - Length int64 - } - mapInfo := func(fi metainfo.FileInfo) fileInfo { - return fileInfo{ - Path: strings.Join(fi.BestPath(), "/"), - Length: fi.Length, - } - } - - existingFiles := apply(existingFiles, mapInfo) - newFiles := apply(newFiles, mapInfo) - - for _, n := range newFiles { - if slices.Contains(existingFiles, n) { - continue - } - - for _, e := range existingFiles { - if e.Path == n.Path && e.Length != n.Length { - log.Warn(ctx, "torrents not compatible, has files with different length", - slog.String("path", n.Path), - slog.Int64("existing-length", e.Length), - slog.Int64("new-length", e.Length), - ) - return false - } - } - } - } - - return true -}