main: dedupe followed .torrent files by infohash

a .torrent fetched over http is added to the shared client in a second
pass (followUps); two that name the same torrent would share one torrent
on the refcount-less client, and the first to finish would Drop it out
from under the other, stranding it at 0%. dedupe the followed files by
infohash, the guard build() already applies to pass-1 sources.
This commit is contained in:
2026-06-20 19:28:32 +09:00
parent 3d9ea7ce8b
commit ee66235c36
2 changed files with 115 additions and 12 deletions

View File

@@ -11,6 +11,9 @@ import (
"syscall"
"testing"
"github.com/anacrolix/torrent/bencode"
"github.com/anacrolix/torrent/metainfo"
"github.com/hanbok/got/bt"
"github.com/hanbok/got/cli"
"github.com/hanbok/got/download"
"github.com/hanbok/got/httpdl"
@@ -112,8 +115,8 @@ func TestDedupeTorrents(t *testing.T) {
}
// TestGatherURIsGrouping checks the command-line grouping: plain http URIs
// become one mirrored download by default, -Z splits them, and torrents/magnets
// stay separate.
// become one mirrored download by default, -Z splits them, and torrents,
// magnets, and .torrent URLs stay separate.
func TestGatherURIsGrouping(t *testing.T) {
parse := func(args ...string) []target {
res, err := cli.Parse(append([]string{"--no-conf"}, args...))
@@ -131,6 +134,16 @@ func TestGatherURIsGrouping(t *testing.T) {
if ts := parse("http://a/x", "magnet:?xt=urn:btih:abc"); len(ts) != 2 {
t.Errorf("http + magnet: got %d targets, want 2 (magnet never mirrors)", len(ts))
}
// Distinct .torrent URLs are separate downloads, not mirrors of one file
// (follow-torrent defaults on, so each is fetched then followed).
if ts := parse("http://a/x.torrent", "http://b/y.torrent", "http://c/z.torrent"); len(ts) != 3 {
t.Errorf(".torrent URLs: got %d targets, want 3 separate downloads", len(ts))
}
// A plain http URL still mirror-groups even alongside a .torrent URL: one
// http group of 1 + one torrent target = 2.
if ts := parse("http://a/x", "http://b/y.torrent"); len(ts) != 2 {
t.Errorf("http + .torrent: got %d targets, want 2", len(ts))
}
}
// TestReadInputFileTAB checks the input-file grouping: TAB-separated URIs on
@@ -152,6 +165,65 @@ func TestReadInputFileTAB(t *testing.T) {
}
}
// writeTorrent creates a .torrent file at dir/name whose info dict is built from
// a file of the given content and internal name. Two calls with the same
// internalName and content produce the same infohash regardless of the output
// filename, so the test can make two .torrent files that name one torrent.
func writeTorrent(t *testing.T, dir, name, internalName string, content []byte) string {
t.Helper()
data := filepath.Join(dir, internalName)
if err := os.WriteFile(data, content, 0o644); err != nil {
t.Fatal(err)
}
info := metainfo.Info{PieceLength: 32 * 1024}
if err := info.BuildFromFilePath(data); err != nil {
t.Fatal(err)
}
b, err := bencode.Marshal(info)
if err != nil {
t.Fatal(err)
}
tp := filepath.Join(dir, name)
f, err := os.Create(tp)
if err != nil {
t.Fatal(err)
}
defer f.Close()
if err := (&metainfo.MetaInfo{InfoBytes: b}).Write(f); err != nil {
t.Fatal(err)
}
return tp
}
// TestFollowUpInfoHashDedup covers the fix for two fetched .torrent files that
// name the same torrent: the second must be flagged a duplicate so followUps
// fails it rather than letting both share — and Drop — one torrent on the client.
func TestFollowUpInfoHashDedup(t *testing.T) {
dir := t.TempDir()
a := writeTorrent(t, dir, "a.torrent", "same", []byte("AAAAAAAA"))
b := writeTorrent(t, dir, "b.torrent", "same", []byte("AAAAAAAA")) // same infohash as a
c := writeTorrent(t, dir, "c.torrent", "diff", []byte("BBBBBBBB")) // a distinct torrent
// Sanity-check the fixture: a and b really share an infohash, c differs.
ha, _ := bt.SourceInfoHash(a, true)
hb, _ := bt.SourceInfoHash(b, true)
hc, _ := bt.SourceInfoHash(c, true)
if ha != hb || ha == hc {
t.Fatalf("fixture: want a==b!=c infohashes, got a=%v b=%v c=%v", ha, hb, hc)
}
seen := map[metainfo.Hash]string{}
if first, dup := markInfoHash(seen, a); dup {
t.Fatalf("a is the first occurrence, wrongly flagged a duplicate of %s", first)
}
if first, dup := markInfoHash(seen, b); !dup || first != a {
t.Errorf("b should duplicate a: got first=%q dup=%v, want %q true", first, dup, a)
}
if _, dup := markInfoHash(seen, c); dup {
t.Errorf("c is a distinct torrent, wrongly flagged a duplicate")
}
}
func TestReportExit(t *testing.T) {
canceled := download.Result{Name: "a", Err: context.Canceled}
done := download.Result{Name: "b", Err: nil}