From 47e0028bc108bdace2138d48aaec4d40618fbc61 Mon Sep 17 00:00:00 2001 From: Alexey Kostin Date: Sun, 17 Apr 2022 16:01:18 +0300 Subject: [PATCH] Fixstructures (#39) * Fix structures * torrents v2 file tree interface * get files vrom both v1 and v2 torrents * fix typo * style * cleanup makefile * new test fileset with real qBittorrent files and new tests * counting files using torrent file * Handle v2 and hybryd torrents --- Makefile | 14 +- internal/transfer/fastresumeHandle.go | 4 +- internal/transfer/transfer.go | 46 +-- internal/transfer/transfer_test.go | 272 +++++++++++------- pkg/helpers/helpers_test.go | 30 ++ pkg/qBittorrentStructures/qBittorrent.go | 116 ++++---- pkg/qBittorrentStructures/qBittorrent_test.go | 56 ++++ pkg/torrentStructures/functions.go | 85 ++++++ pkg/torrentStructures/functions_test.go | 227 +++++++++++++++ pkg/torrentStructures/torrent.go | 54 ++-- test/data/testdir_hybrid.fastresume | Bin 0 -> 1610 bytes test/data/testdir_hybrid.torrent | Bin 0 -> 1883 bytes test/data/testdir_v1.fastresume | Bin 0 -> 1191 bytes test/data/testdir_v1.torrent | 1 + test/data/testdir_v2.fastresume | Bin 0 -> 1622 bytes test/data/testdir_v2.torrent | 10 + test/data/testfile1_single_hybrid.fastresume | Bin 0 -> 1250 bytes test/data/testfile1_single_hybrid.torrent | Bin 0 -> 290 bytes test/data/testfile1_single_v1.fastresume | Bin 0 -> 1188 bytes test/data/testfile1_single_v1.torrent | Bin 0 -> 159 bytes test/data/testfile1_single_v2.fastresume | Bin 0 -> 1262 bytes test/data/testfile1_single_v2.torrent | 2 + 22 files changed, 689 insertions(+), 228 deletions(-) create mode 100644 pkg/qBittorrentStructures/qBittorrent_test.go create mode 100644 pkg/torrentStructures/functions.go create mode 100644 pkg/torrentStructures/functions_test.go create mode 100644 test/data/testdir_hybrid.fastresume create mode 100644 test/data/testdir_hybrid.torrent create mode 100644 test/data/testdir_v1.fastresume create mode 100644 test/data/testdir_v1.torrent create mode 100644 test/data/testdir_v2.fastresume create mode 100644 test/data/testdir_v2.torrent create mode 100644 test/data/testfile1_single_hybrid.fastresume create mode 100644 test/data/testfile1_single_hybrid.torrent create mode 100644 test/data/testfile1_single_v1.fastresume create mode 100644 test/data/testfile1_single_v1.torrent create mode 100644 test/data/testfile1_single_v2.fastresume create mode 100644 test/data/testfile1_single_v2.torrent diff --git a/Makefile b/Makefile index ee3f3b3..ef05d92 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,4 @@ -#GOOS=windows GOACH=amd64 go build -o bt2qbt_v${1}_amd64.exe -tags forceposix -#GOOS=windows GOARCH=386 go build -o bt2qbt_v${1}_i386.exe -tags forceposix -#GOOS=linux GOARCH=amd64 go build -o bt2qbt_v${1}_amd64_linux -tags forceposix -#GOOS=linux GOARCH=386 go build -o bt2qbt_v${1}_i386_linux -tags forceposix -#GOOS=darwin GOARCH=amd64 go build -o bt2qbt_v${1}_amd64_macos -tags forceposix -#GOOS=darwin GOARCH=386 go build -o bt2qbt_v${1}_i386_macos -tags forceposix - -gotag=1.18.0-bullseye +gotag=1.18.1-bullseye commit=$(shell git rev-parse HEAD) @@ -15,13 +8,12 @@ buildenvs = -e CGO_ENABLED=0 version = 1.999 ldflags = -ldflags="-X 'main.version=$(version)' -X 'main.commit=$(commit)' -X 'main.buildImage=golang:$(gotag)'" -all: tests build +all: | tests build tests: $(dockercmd) golang:$(gotag) go test $(buildtags) ./... -build: | tests windows linux darwin - +build: windows linux darwin windows: $(dockercmd) $(buildenvs) -e GOOS=windows -e GOARCH=amd64 golang:$(gotag) go build -v $(buildtags) $(ldflags) -o bt2qbt_v$(version)_amd64.exe diff --git a/internal/transfer/fastresumeHandle.go b/internal/transfer/fastresumeHandle.go index b1c14a0..199fd30 100644 --- a/internal/transfer/fastresumeHandle.go +++ b/internal/transfer/fastresumeHandle.go @@ -17,9 +17,10 @@ func (transfer *TransferStructure) HandleStructures() { transfer.Fastresume.ActiveTime = transfer.ResumeItem.Runtime transfer.Fastresume.AddedTime = transfer.ResumeItem.AddedOn transfer.Fastresume.CompletedTime = transfer.ResumeItem.CompletedOn - transfer.Fastresume.Info = transfer.TorrentFile.Info + transfer.Fastresume.Info = transfer.TorrentFileRaw["info"] transfer.Fastresume.InfoHash = transfer.ResumeItem.Info transfer.Fastresume.SeedingTime = transfer.ResumeItem.Runtime + transfer.HandlePriority() // handle priorities before handling pieces and state transfer.HandleState() transfer.Fastresume.FinishedTime = int64(time.Since(time.Unix(transfer.ResumeItem.CompletedOn, 0)).Minutes()) @@ -30,7 +31,6 @@ func (transfer *TransferStructure) HandleStructures() { transfer.HandleLabels() transfer.HandleTrackers() - transfer.HandlePriority() // important handle priorities before handling pieces /* pieces maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, diff --git a/internal/transfer/transfer.go b/internal/transfer/transfer.go index 39357db..5118879 100644 --- a/internal/transfer/transfer.go +++ b/internal/transfer/transfer.go @@ -87,20 +87,19 @@ func (transfer *TransferStructure) HandleState() { transfer.Fastresume.Paused = 1 transfer.Fastresume.AutoManaged = 0 } else { - if transfer.TorrentFile.Info.Files != nil { - if len(transfer.TorrentFile.Info.Files) > 1 { - var parted bool - for _, prio := range transfer.ResumeItem.Prio { - if byte(prio) == 0 || byte(prio) == 128 { - parted = true - } - } - if parted { - transfer.Fastresume.Paused = 1 - transfer.Fastresume.AutoManaged = 0 - return + if len(transfer.TorrentFile.GetFileList()) > 1 { + var parted bool + for _, prio := range transfer.Fastresume.FilePriority { + if prio == 0 { + parted = true + break } } + if parted { + transfer.Fastresume.Paused = 1 + transfer.Fastresume.AutoManaged = 0 + return + } } transfer.Fastresume.Paused = 0 transfer.Fastresume.AutoManaged = 1 @@ -170,6 +169,13 @@ func (transfer *TransferStructure) HandleTrackers() { } func (transfer *TransferStructure) HandlePriority() { + if transfer.TorrentFile.IsV2OrHybryd() { // so we need get only odd + trimmedPrio := make([]byte, 0, len(transfer.ResumeItem.Prio)/2) + for i := 0; i < len(transfer.ResumeItem.Prio); i += 2 { + trimmedPrio = append(trimmedPrio, transfer.ResumeItem.Prio[i]) + } + transfer.ResumeItem.Prio = trimmedPrio + } for _, c := range transfer.ResumeItem.Prio { if i := int(c); (i == 0) || (i == 128) { // if priority not selected transfer.Fastresume.FilePriority = append(transfer.Fastresume.FilePriority, 0) @@ -195,7 +201,7 @@ func (transfer *TransferStructure) HandlePieces() { if transfer.Fastresume.Unfinished != nil { transfer.FillWholePieces(0) } else { - if len(transfer.TorrentFile.Info.Files) > 0 { + if len(transfer.TorrentFile.GetFileList()) > 0 { transfer.FillPiecesParted() } else { transfer.FillWholePieces(1) @@ -220,7 +226,7 @@ func (transfer *TransferStructure) FillPiecesParted() { } var fileOffsets []Offset bytesLength := int64(0) - for _, file := range transfer.TorrentFile.Info.Files { + for _, file := range transfer.TorrentFile.GetFileListWB() { // need to adapt for torrents v2 version fileFirstOffset := bytesLength + 1 bytesLength += file.Length fileLastOffset := bytesLength @@ -272,7 +278,7 @@ func (transfer *TransferStructure) HandleSavePaths() { } lastPathName := fileHelpers.Base(transfer.ResumeItem.Path) - if len(transfer.TorrentFile.Info.Files) > 0 { + if len(transfer.TorrentFile.GetFileList()) > 0 { if lastPathName == torrentName { transfer.Fastresume.QBtContentLayout = "Original" transfer.Fastresume.QbtSavePath = fileHelpers.CutLastPath(transfer.ResumeItem.Path, transfer.Opts.PathSeparator) @@ -300,14 +306,8 @@ func (transfer *TransferStructure) HandleSavePaths() { transfer.Fastresume.QBtContentLayout = "NoSubfolder" // NoSubfolder always has full mapped files // so we append all of them - for _, filePath := range transfer.TorrentFile.Info.Files { - var paths []string - if len(filePath.PathUTF8) != 0 { - paths = filePath.PathUTF8 - } else { - paths = filePath.Path - } - transfer.Fastresume.MappedFiles = append(transfer.Fastresume.MappedFiles, fileHelpers.Join(paths, transfer.Opts.PathSeparator)) + for _, filePath := range transfer.TorrentFile.GetFileList() { + transfer.Fastresume.MappedFiles = append(transfer.Fastresume.MappedFiles, fileHelpers.Normalize(filePath, transfer.Opts.PathSeparator)) } // and then doing remap if resumeItem contain target field if maxIndex := transfer.FindHighestIndexOfMappedFiles(); maxIndex >= 0 { diff --git a/internal/transfer/transfer_test.go b/internal/transfer/transfer_test.go index 3e24c44..138f424 100644 --- a/internal/transfer/transfer_test.go +++ b/internal/transfer/transfer_test.go @@ -921,11 +921,11 @@ func TestTransferStructure_HandlePieces(t *testing.T) { TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ Files: []*torrentStructures.TorrentFile{ - &torrentStructures.TorrentFile{Length: 5}, - &torrentStructures.TorrentFile{Length: 5}, - &torrentStructures.TorrentFile{Length: 5}, - &torrentStructures.TorrentFile{Length: 5}, - &torrentStructures.TorrentFile{Length: 5}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5}, }, PieceLength: 5, }, @@ -954,9 +954,9 @@ func TestTransferStructure_HandlePieces(t *testing.T) { TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ Files: []*torrentStructures.TorrentFile{ - &torrentStructures.TorrentFile{Length: 13}, - &torrentStructures.TorrentFile{Length: 7}, - &torrentStructures.TorrentFile{Length: 5}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 13}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 7}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5}, }, PieceLength: 5, // 25 total }, @@ -985,9 +985,9 @@ func TestTransferStructure_HandlePieces(t *testing.T) { TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ Files: []*torrentStructures.TorrentFile{ - &torrentStructures.TorrentFile{Length: 9}, - &torrentStructures.TorrentFile{Length: 6}, - &torrentStructures.TorrentFile{Length: 10}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 9}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 6}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 10}, }, PieceLength: 5, // 25 total }, @@ -1016,9 +1016,9 @@ func TestTransferStructure_HandlePieces(t *testing.T) { TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ Files: []*torrentStructures.TorrentFile{ - &torrentStructures.TorrentFile{Length: 13}, - &torrentStructures.TorrentFile{Length: 7}, - &torrentStructures.TorrentFile{Length: 5}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 13}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 7}, + &torrentStructures.TorrentFile{Path: []string{`/`}, Length: 5}, }, PieceLength: 5, // 25 total }, @@ -1102,25 +1102,93 @@ func TestTransferStructure_HandlePieces(t *testing.T) { } func TestTransferStructure_HandlePriority(t *testing.T) { - transferStructure := TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{FilePriority: []int64{}}, - ResumeItem: &utorrentStructs.ResumeItem{ - Prio: []byte{ - byte(0), - byte(128), - byte(2), - byte(5), - byte(8), - byte(9), - byte(15), - byte(127), // unexpected + + type HandlePriorityCase struct { + name string + mustFail bool + newTransferStructure *TransferStructure + expected []int64 + } + cases := []HandlePriorityCase{ + { + name: "001 mustfail", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{FilePriority: []int64{}}, + TorrentFile: &torrentStructures.Torrent{Info: &torrentStructures.TorrentInfo{}}, + ResumeItem: &utorrentStructs.ResumeItem{ + Prio: []byte{}, + }, }, + mustFail: true, + expected: []int64{0, 0, 1, 1, 1, 6, 6, 0}, + }, + { + name: "002 check priotiry for v1 torrents", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{FilePriority: []int64{}}, + TorrentFile: &torrentStructures.Torrent{Info: &torrentStructures.TorrentInfo{}}, + ResumeItem: &utorrentStructs.ResumeItem{ + Prio: []byte{ + byte(0), + byte(128), + byte(2), + byte(5), + byte(8), + byte(9), + byte(15), + byte(127), // unexpected + }, + }, + }, + expected: []int64{0, 0, 1, 1, 1, 6, 6, 0}, + }, + { + name: "002 check priotiry for v2 torrents", + newTransferStructure: &TransferStructure{ + Fastresume: &qBittorrentStructures.QBittorrentFastresume{FilePriority: []int64{}}, + TorrentFile: &torrentStructures.Torrent{ + Info: &torrentStructures.TorrentInfo{ + FileTree: map[string]interface{}{}, + }, + }, + ResumeItem: &utorrentStructs.ResumeItem{ + Prio: []byte{ + byte(0), + byte(128), + byte(128), + byte(128), + byte(2), + byte(128), + byte(5), + byte(128), + byte(8), + byte(128), + byte(9), + byte(128), + byte(15), + byte(128), + byte(127), // unexpected + byte(128), + }, + }, + }, + expected: []int64{0, 0, 1, 1, 1, 6, 6, 0}, }, } - expect := []int64{0, 0, 1, 1, 1, 6, 6, 0} - transferStructure.HandlePriority() - if !reflect.DeepEqual(transferStructure.Fastresume.FilePriority, expect) { - t.Fatalf("Unexpected error: opts isn't equal:\n Got: %#v\n Expect %#v\n", transferStructure.Fastresume.FilePriority, expect) + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + testCase.newTransferStructure.HandlePriority() + equal := reflect.DeepEqual(testCase.expected, testCase.newTransferStructure.Fastresume.FilePriority) + if !equal && !testCase.mustFail { + changes, err := diff.Diff(testCase.newTransferStructure.Fastresume.FilePriority, testCase.expected, diff.DiscardComplexOrigin()) + if err != nil { + t.Error(err.Error()) + } + t.Fatalf("Unexpected error: opts isn't equal:\n Got: %#v\n Expect %#v\n Diff: %v\n", testCase.newTransferStructure.Fastresume.FilePriority, testCase.expected, spew.Sdump(changes)) + } else if equal && testCase.mustFail { + t.Fatalf("Unexpected error: structures are equal, but they shouldn't\n Got: %v\n", spew.Sdump(testCase.newTransferStructure.Fastresume.FilePriority)) + } + }) } } @@ -1239,22 +1307,17 @@ func TestTransferStructure_HandleState(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, ResumeItem: &utorrentStructs.ResumeItem{ Started: 1, - Prio: []byte{ - byte(1), - byte(1), - byte(2), - byte(5), - byte(8), - byte(9), - byte(15), - }, }, TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ Files: []*torrentStructures.TorrentFile{ - &torrentStructures.TorrentFile{}, - &torrentStructures.TorrentFile{}, - &torrentStructures.TorrentFile{}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, }, }, }, @@ -1266,121 +1329,125 @@ func TestTransferStructure_HandleState(t *testing.T) { { name: "005 started resume with parted downloaded files", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + FilePriority: []int64{1, 0, 1, 1, 1, 6, 6}, + }, ResumeItem: &utorrentStructs.ResumeItem{ Started: 1, - Prio: []byte{ - byte(0), - byte(10), - byte(2), - byte(5), - byte(8), - byte(9), - byte(15), - }, }, TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ Files: []*torrentStructures.TorrentFile{ - &torrentStructures.TorrentFile{}, - &torrentStructures.TorrentFile{}, - &torrentStructures.TorrentFile{}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, }, }, }, }, expected: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{Paused: 1, AutoManaged: 0}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + Paused: 1, + AutoManaged: 0, + FilePriority: []int64{1, 0, 1, 1, 1, 6, 6}, + }, }, }, { name: "006 started resume with parted downloaded files", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + FilePriority: []int64{1, 0, 1, 1, 1, 6, 6}, + }, ResumeItem: &utorrentStructs.ResumeItem{ Started: 1, - Prio: []byte{ - byte(1), - byte(128), - byte(2), - byte(5), - byte(8), - byte(9), - byte(15), - }, }, TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ Files: []*torrentStructures.TorrentFile{ - &torrentStructures.TorrentFile{}, - &torrentStructures.TorrentFile{}, - &torrentStructures.TorrentFile{}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, }, }, }, }, expected: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{Paused: 1, AutoManaged: 0}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + Paused: 1, + AutoManaged: 0, + FilePriority: []int64{1, 0, 1, 1, 1, 6, 6}, + }, }, }, { name: "007 started resume with full downloaded files", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + FilePriority: []int64{1, 0, 1, 1, 1, 6, 6}, + }, ResumeItem: &utorrentStructs.ResumeItem{ Started: 1, - Prio: []byte{ - byte(1), - byte(128), - byte(2), - byte(5), - byte(8), - byte(9), - byte(15), - }, }, TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ Files: []*torrentStructures.TorrentFile{ - &torrentStructures.TorrentFile{}, - &torrentStructures.TorrentFile{}, - &torrentStructures.TorrentFile{}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, }, }, }, }, expected: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{Paused: 1, AutoManaged: 0}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + Paused: 1, + AutoManaged: 0, + FilePriority: []int64{1, 0, 1, 1, 1, 6, 6}, + }, }, }, { name: "008 started resume with parted downloaded files", newTransferStructure: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + FilePriority: []int64{1, 0, 1, 1, 1, 6, 6}, + }, ResumeItem: &utorrentStructs.ResumeItem{ Started: 1, - Prio: []byte{ - byte(1), - byte(128), - byte(2), - byte(5), - byte(8), - byte(9), - byte(15), - }, }, TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{ Files: []*torrentStructures.TorrentFile{ - &torrentStructures.TorrentFile{}, - &torrentStructures.TorrentFile{}, - &torrentStructures.TorrentFile{}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, + &torrentStructures.TorrentFile{Path: []string{`/`}}, }, }, }, }, expected: &TransferStructure{ - Fastresume: &qBittorrentStructures.QBittorrentFastresume{Paused: 1, AutoManaged: 0}, + Fastresume: &qBittorrentStructures.QBittorrentFastresume{ + Paused: 1, + AutoManaged: 0, + FilePriority: []int64{1, 0, 1, 1, 1, 6, 6}, + }, }, }, { @@ -1389,15 +1456,6 @@ func TestTransferStructure_HandleState(t *testing.T) { Fastresume: &qBittorrentStructures.QBittorrentFastresume{}, ResumeItem: &utorrentStructs.ResumeItem{ Started: 0, - Prio: []byte{ - byte(1), - byte(128), - byte(2), - byte(5), - byte(8), - byte(9), - byte(15), - }, }, TorrentFile: &torrentStructures.Torrent{ Info: &torrentStructures.TorrentInfo{}, diff --git a/pkg/helpers/helpers_test.go b/pkg/helpers/helpers_test.go index b8027e9..24f6fea 100644 --- a/pkg/helpers/helpers_test.go +++ b/pkg/helpers/helpers_test.go @@ -27,3 +27,33 @@ func TestGetStrings(t *testing.T) { t.Fatalf("Unexpected error: opts isn't equal:\n Got: %#v\n Expect %#v\n", trackers, expect) } } + +func TestDecodeTorrentFile(t *testing.T) { + type PathJoinCase struct { + name string + mustFail bool + path string + } + cases := []PathJoinCase{ + { + name: "001 not existing file", + path: "notexists.torrent", + mustFail: true, + }, + { + name: "002 existing file", + path: "../../test/data/testfileset.torrent", + }, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + var decoded interface{} + err := DecodeTorrentFile(testCase.path, &decoded) + if err != nil && !testCase.mustFail { + t.Fatalf("Unexpected error: %v", err) + } else if err == nil && testCase.mustFail { + t.Fatalf("Test must fail, but it doesn't") + } + }) + } +} diff --git a/pkg/qBittorrentStructures/qBittorrent.go b/pkg/qBittorrentStructures/qBittorrent.go index 65cdd5f..759101a 100644 --- a/pkg/qBittorrentStructures/qBittorrent.go +++ b/pkg/qBittorrentStructures/qBittorrent.go @@ -1,65 +1,63 @@ package qBittorrentStructures -import "github.com/rumanzo/bt2qbt/pkg/torrentStructures" - // https://www.libtorrent.org/manual-ref.html type QBittorrentFastresume struct { - ActiveTime int64 `bencode:"active_time"` // integer. The number of seconds this torrent has been active. i.e. not paused. - AddedTime int64 `bencode:"added_time"` - Allocation string `bencode:"allocation"` // The allocation mode for the storage. Can be either allocate or sparse. - ApplyIpFilter int64 `bencode:"apply_ip_filter"` // integer. 1 if the torrent_flags::apply_ip_filter is set. - AutoManaged int64 `bencode:"auto_managed"` // integer. 1 if the torrent is auto managed, otherwise 0. - BannedPeers []byte `bencode:"banned_peers"` // string. This string has the same format as peers but instead represent IPv4 peers that we have banned. - BannedPeers6 []byte `bencode:"banned_peers6"` // string. This string has the same format as peers6 but instead represent IPv6 peers that we have banned. - CompletedTime int64 `bencode:"completed_time"` - DisableDht int64 `bencode:"disable_dht"` // integer. 1 if the torrent_flags::disable_dht is set. - DisableLsd int64 `bencode:"disable_lsd"` // integer. 1 if the torrent_flags::disable_lsd is set. - DisablePex int64 `bencode:"disable_pex"` // integer. 1 if the torrent_flags::disable_pex is set. - DownloadRateLimit int64 `bencode:"download_rate_limit"` // integer. The download rate limit for this torrent in case one is set, in bytes per second. - FileFormat string `bencode:"file-format"` // string: "libtorrent resume file" - FilePriority []int64 `bencode:"file_priority"` // list of integers. One entry per file in the torrent. Each entry is the priority of the file with the same index. - FileVersion int64 `bencode:"file-version"` - FinishedTime int64 `bencode:"finished_time"` - HttpSeeds []string `bencode:"httpseeds"` // list of strings. List of HTTP seed URLs used by this torrent. The URLs are expected to be properly encoded and not contain any illegal url characters. - Info *torrentStructures.TorrentInfo `bencode:"info,omitempty"` // If this field is present, it should be the info-dictionary of the torrent this resume data is for. Its SHA-1 hash must match the one in the info-hash field. When present, the torrent is loaded from here, meaning the torrent can be added purely from resume data (no need to load the .torrent file separately). This may have performance advantages. - InfoHash string `bencode:"info-hash"` // string, the info hash of the torrent this data is saved for. This is a 20 byte SHA-1 hash of the info section of the torrent if this is a v1 or v1+v2-hybrid torrent. - InfoHash2 string `bencode:"info-hash2"` // string, the v2 info hash of the torrent this data is saved. for, in case it is a v2 or v1+v2-hybrid torrent. This is a 32 byte SHA-256 hash of the info section of the torrent. - LastDownload int64 `bencode:"last_download"` // integer. The number of seconds since epoch when we last downloaded payload from a peer on this torrent. - LastSeenComplete int64 `bencode:"last_seen_complete"` - LastUpload int64 `bencode:"last_upload"` // integer. The number of seconds since epoch when we last uploaded payload to a peer on this torrent. - LibTorrentVersion string `bencode:"libtorrent-version"` - MappedFiles []string `bencode:"mapped_files,omitempty"` // list of strings. If any file in the torrent has been renamed, this entry contains a list of all the filenames. In the same order as in the torrent file. - MaxConnections int64 `bencode:"max_connections"` // integer. The max number of peer connections this torrent may have, if a limit is set. - MaxUploads int64 `bencode:"max_uploads"` // integer. The max number of unchoked peers this torrent may have, if a limit is set. - NumComplete int64 `bencode:"num_complete"` - NumDownloaded int64 `bencode:"num_downloaded"` - NumIncomplete int64 `bencode:"num_incomplete"` - Paused int64 `bencode:"paused"` // integer. 1 if the torrent is paused, 0 otherwise. - Peers int64 `bencode:"peers"` // string. This string contains IPv4 and port pairs of peers we were connected to last session. The endpoints are in compact representation. 4 bytes IPv4 address followed by 2 bytes port. Hence, the length of this string should be divisible by 6. - Peers6 int64 `bencode:"peers6"` // string. This string contains IPv6 and port pairs of peers we were connected to last session. The endpoints are in compact representation. 16 bytes IPv6 address followed by 2 bytes port. The length of this string should be divisible by 18. - Pieces []byte `bencode:"pieces"` // A string with piece flags, one character per piece. Bit 1 means we have that piece. Bit 2 means we have verified that this piece is correct. This only applies when the torrent is in seed_mode. - QBtCategory string `bencode:"qBt-category"` - QBtContentLayout string `bencode:"qBt-contentLayout"` // Original, Subfolder, NoSubfolder - QBtFirstLastPiecePriority string `bencode:"qBt-firstLastPiecePriority"` - QbtName string `bencode:"qBt-name"` - QbtRatioLimit int64 `bencode:"qBt-ratioLimit"` - QbtSavePath string `bencode:"qBt-savePath"` - QbtSeedStatus int64 `bencode:"qBt-seedStatus"` - QbtSeedingTimeLimit int64 `bencode:"qBt-seedingTimeLimit"` - QbtTags []string `bencode:"qBt-tags"` - SavePath string `bencode:"save_path"` // string. The save path where this torrent was saved. This is especially useful when moving torrents with move_storage() since this will be updated. - SeedMode int64 `bencode:"seed_mode"` // integer. 1 if the torrent is in seed mode, 0 otherwise. - SeedingTime int64 `bencode:"seeding_time"` // integer. The number of seconds this torrent has been active and seeding. - SequentialDownload int64 `bencode:"sequential_download"` // integer. 1 if the torrent is in sequential download mode, 0 otherwise. - ShareMode int64 `bencode:"share_mode"` // integer. 1 if the torrent_flags::share_mode is set. - StopWhenReady int64 `bencode:"stop_when_ready"` // integer. 1 if the torrent_flags::stop_when_ready is set. - SuperSeeding int64 `bencode:"super_seeding"` // integer. 1 if the torrent_flags::super_seeding is set. - TotalDownloaded int64 `bencode:"total_downloaded"` // integer. The number of bytes that have been downloaded in total for this torrent. - TotalUploaded int64 `bencode:"total_uploaded"` // integer. The number of bytes that have been uploaded in total for this torrent. - Trackers [][]string `bencode:"trackers"` // list of lists of strings. The top level list lists all tracker tiers. Each second level list is one tier of trackers. - Unfinished *[]interface{} `bencode:"unfinished,omitempty"` - UploadMode int64 `bencode:"upload_mode"` // integer. 1 if the torrent_flags::upload_mode is set. - UploadRateLimit int64 `bencode:"upload_rate_limit"` // integer. In case this torrent has a per-torrent upload rate limit, this is that limit. In bytes per second. - UrlList int64 `bencode:"url-list"` // list of strings. List of url-seed URLs used by this torrent. The URLs are expected to be properly encoded and not contain any illegal url characters. + ActiveTime int64 `bencode:"active_time"` // integer. The number of seconds this torrent has been active. i.e. not paused. + AddedTime int64 `bencode:"added_time"` + Allocation string `bencode:"allocation"` // The allocation mode for the storage. Can be either allocate or sparse. + ApplyIpFilter int64 `bencode:"apply_ip_filter"` // integer. 1 if the torrent_flags::apply_ip_filter is set. + AutoManaged int64 `bencode:"auto_managed"` // integer. 1 if the torrent is auto managed, otherwise 0. + BannedPeers []byte `bencode:"banned_peers"` // string. This string has the same format as peers but instead represent IPv4 peers that we have banned. + BannedPeers6 []byte `bencode:"banned_peers6"` // string. This string has the same format as peers6 but instead represent IPv6 peers that we have banned. + CompletedTime int64 `bencode:"completed_time"` + DisableDht int64 `bencode:"disable_dht"` // integer. 1 if the torrent_flags::disable_dht is set. + DisableLsd int64 `bencode:"disable_lsd"` // integer. 1 if the torrent_flags::disable_lsd is set. + DisablePex int64 `bencode:"disable_pex"` // integer. 1 if the torrent_flags::disable_pex is set. + DownloadRateLimit int64 `bencode:"download_rate_limit"` // integer. The download rate limit for this torrent in case one is set, in bytes per second. + FileFormat string `bencode:"file-format"` // string: "libtorrent resume file" + FilePriority []int64 `bencode:"file_priority"` // list of integers. One entry per file in the torrent. Each entry is the priority of the file with the same index. + FileVersion int64 `bencode:"file-version"` + FinishedTime int64 `bencode:"finished_time"` + HttpSeeds []string `bencode:"httpseeds"` // list of strings. List of HTTP seed URLs used by this torrent. The URLs are expected to be properly encoded and not contain any illegal url characters. + Info interface{} `bencode:"info,omitempty"` // If this field is present, it should be the info-dictionary of the torrent this resume data is for. Its SHA-1 hash must match the one in the info-hash field. When present, the torrent is loaded from here, meaning the torrent can be added purely from resume data (no need to load the .torrent file separately). This may have performance advantages. + InfoHash string `bencode:"info-hash"` // string, the info hash of the torrent this data is saved for. This is a 20 byte SHA-1 hash of the info section of the torrent if this is a v1 or v1+v2-hybrid torrent. + InfoHash2 string `bencode:"info-hash2"` // string, the v2 info hash of the torrent this data is saved. for, in case it is a v2 or v1+v2-hybrid torrent. This is a 32 byte SHA-256 hash of the info section of the torrent. + LastDownload int64 `bencode:"last_download"` // integer. The number of seconds since epoch when we last downloaded payload from a peer on this torrent. + LastSeenComplete int64 `bencode:"last_seen_complete"` + LastUpload int64 `bencode:"last_upload"` // integer. The number of seconds since epoch when we last uploaded payload to a peer on this torrent. + LibTorrentVersion string `bencode:"libtorrent-version"` + MappedFiles []string `bencode:"mapped_files,omitempty"` // list of strings. If any file in the torrent has been renamed, this entry contains a list of all the filenames. In the same order as in the torrent file. + MaxConnections int64 `bencode:"max_connections"` // integer. The max number of peer connections this torrent may have, if a limit is set. + MaxUploads int64 `bencode:"max_uploads"` // integer. The max number of unchoked peers this torrent may have, if a limit is set. + NumComplete int64 `bencode:"num_complete"` + NumDownloaded int64 `bencode:"num_downloaded"` + NumIncomplete int64 `bencode:"num_incomplete"` + Paused int64 `bencode:"paused"` // integer. 1 if the torrent is paused, 0 otherwise. + Peers string `bencode:"peers"` // string. This string contains IPv4 and port pairs of peers we were connected to last session. The endpoints are in compact representation. 4 bytes IPv4 address followed by 2 bytes port. Hence, the length of this string should be divisible by 6. + Peers6 string `bencode:"peers6"` // string. This string contains IPv6 and port pairs of peers we were connected to last session. The endpoints are in compact representation. 16 bytes IPv6 address followed by 2 bytes port. The length of this string should be divisible by 18. + Pieces []byte `bencode:"pieces"` // A string with piece flags, one character per piece. Bit 1 means we have that piece. Bit 2 means we have verified that this piece is correct. This only applies when the torrent is in seed_mode. + QBtCategory string `bencode:"qBt-category"` + QBtContentLayout string `bencode:"qBt-contentLayout"` // Original, Subfolder, NoSubfolder + QBtFirstLastPiecePriority int64 `bencode:"qBt-firstLastPiecePriority"` + QbtName string `bencode:"qBt-name"` + QbtRatioLimit int64 `bencode:"qBt-ratioLimit"` + QbtSavePath string `bencode:"qBt-savePath"` + QbtSeedStatus int64 `bencode:"qBt-seedStatus"` + QbtSeedingTimeLimit int64 `bencode:"qBt-seedingTimeLimit"` + QbtTags []string `bencode:"qBt-tags"` + SavePath string `bencode:"save_path"` // string. The save path where this torrent was saved. This is especially useful when moving torrents with move_storage() since this will be updated. + SeedMode int64 `bencode:"seed_mode"` // integer. 1 if the torrent is in seed mode, 0 otherwise. + SeedingTime int64 `bencode:"seeding_time"` // integer. The number of seconds this torrent has been active and seeding. + SequentialDownload int64 `bencode:"sequential_download"` // integer. 1 if the torrent is in sequential download mode, 0 otherwise. + ShareMode int64 `bencode:"share_mode"` // integer. 1 if the torrent_flags::share_mode is set. + StopWhenReady int64 `bencode:"stop_when_ready"` // integer. 1 if the torrent_flags::stop_when_ready is set. + SuperSeeding int64 `bencode:"super_seeding"` // integer. 1 if the torrent_flags::super_seeding is set. + TotalDownloaded int64 `bencode:"total_downloaded"` // integer. The number of bytes that have been downloaded in total for this torrent. + TotalUploaded int64 `bencode:"total_uploaded"` // integer. The number of bytes that have been uploaded in total for this torrent. + Trackers [][]string `bencode:"trackers"` // list of lists of strings. The top level list lists all tracker tiers. Each second level list is one tier of trackers. + Unfinished *[]interface{} `bencode:"unfinished,omitempty"` + UploadMode int64 `bencode:"upload_mode"` // integer. 1 if the torrent_flags::upload_mode is set. + UploadRateLimit int64 `bencode:"upload_rate_limit"` // integer. In case this torrent has a per-torrent upload rate limit, this is that limit. In bytes per second. + UrlList []string `bencode:"url-list"` // list of strings. List of url-seed URLs used by this torrent. The URLs are expected to be properly encoded and not contain any illegal url characters. } diff --git a/pkg/qBittorrentStructures/qBittorrent_test.go b/pkg/qBittorrentStructures/qBittorrent_test.go new file mode 100644 index 0000000..33ff802 --- /dev/null +++ b/pkg/qBittorrentStructures/qBittorrent_test.go @@ -0,0 +1,56 @@ +package qBittorrentStructures + +import ( + "github.com/rumanzo/bt2qbt/pkg/helpers" + "testing" +) + +func TestDecodeFastresumeFile(t *testing.T) { + type PathJoinCase struct { + name string + mustFail bool + path string + } + cases := []PathJoinCase{ + { + name: "001 not existing file", + path: "notexists.fastresume", + mustFail: true, + }, + { + name: "002 testdir hybryd", + path: "../../test/data/testdir_hybrid.fastresume", + }, + { + name: "003 testdir v1", + path: "../../test/data/testdir_v1.fastresume", + }, + { + name: "004 testdir v2", + path: "../../test/data/testdir_v2.fastresume", + }, + { + name: "005 single hybryd", + path: "../../test/data/testfile1_single_hybrid.fastresume", + }, + { + name: "006 single v1", + path: "../../test/data/testfile1_single_v1.fastresume", + }, + { + name: "007 single v2", + path: "../../test/data/testfile1_single_v2.fastresume", + }, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + var decoded QBittorrentFastresume + err := helpers.DecodeTorrentFile(testCase.path, &decoded) + if err != nil && !testCase.mustFail { + t.Fatalf("Unexpected error: %v", err) + } else if err == nil && testCase.mustFail { + t.Fatalf("Test must fail, but it doesn't") + } + }) + } +} diff --git a/pkg/torrentStructures/functions.go b/pkg/torrentStructures/functions.go new file mode 100644 index 0000000..215174e --- /dev/null +++ b/pkg/torrentStructures/functions.go @@ -0,0 +1,85 @@ +package torrentStructures + +import ( + "github.com/rumanzo/bt2qbt/pkg/fileHelpers" + "sort" +) + +func (t *Torrent) IsV2OrHybryd() bool { + if t.Info.FileTree != nil { + return true + } + return false +} + +// GetFileListWB function that return struct with filelists with bytes from torrent file +func (t *Torrent) GetFileListWB() []FilepathLength { + if t.FilePathLength == nil { + if t.IsV2OrHybryd() { // torrents with v2 or hybrid scheme + result := getFileListV2(t.Info.FileTree) + t.FilePathLength = &result + return *t.FilePathLength + } else { // torrent v1 with FileTree + result := getFileListV1(t) + t.FilePathLength = &result + return *t.FilePathLength + } + } else { + return *t.FilePathLength + } +} + +func (t *Torrent) GetFileList() []string { + if t.FilePathLength == nil { + t.GetFileListWB() + } + if t.FilePaths == nil { + t.FilePaths = &[]string{} + for _, fb := range *t.FilePathLength { + *t.FilePaths = append(*t.FilePaths, fb.Path) + } + } + return *t.FilePaths +} + +func getFileListV1(t *Torrent) []FilepathLength { + var files []FilepathLength + for _, file := range t.Info.Files { + if file.PathUTF8 != nil { + files = append(files, FilepathLength{ + Path: fileHelpers.Join(file.PathUTF8, `/`), + Length: file.Length, + }) + } else { + files = append(files, FilepathLength{ + Path: fileHelpers.Join(file.Path, `/`), + Length: file.Length, + }) + } + } + return files +} + +func getFileListV2(f interface{}) []FilepathLength { + nfiles := []FilepathLength{} + + // sort map previously + keys := make([]string, 0, len(f.(map[string]interface{}))) + for k := range f.(map[string]interface{}) { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + v := f.(map[string]interface{})[k] + if len(k) == 0 { // it's means that next will be structure with length and piece root + nfiles = append(nfiles, FilepathLength{Path: "", Length: v.(map[string]interface{})["length"].(int64)}) + return nfiles + } + s := getFileListV2(v) + for _, fpl := range s { + nfiles = append(nfiles, FilepathLength{Path: fileHelpers.Join(append([]string{k}, fpl.Path), `/`), Length: fpl.Length}) + } + } + return nfiles +} diff --git a/pkg/torrentStructures/functions_test.go b/pkg/torrentStructures/functions_test.go new file mode 100644 index 0000000..4823cc7 --- /dev/null +++ b/pkg/torrentStructures/functions_test.go @@ -0,0 +1,227 @@ +package torrentStructures + +import ( + "github.com/davecgh/go-spew/spew" + "github.com/r3labs/diff/v2" + "github.com/rumanzo/bt2qbt/pkg/helpers" + "reflect" + "testing" +) + +func TestDecodeRealTorrents(t *testing.T) { + type PathJoinCase struct { + name string + mustFail bool + path string + } + cases := []PathJoinCase{ + { + name: "001 not existing file", + path: "notexists.torrent", + mustFail: true, + }, + { + name: "002 existing file", + path: "../../test/data/testfileset.torrent", + }, + { + name: "003 testdir hybryd", + path: "../../test/data/testdir_hybrid.torrent", + }, + { + name: "004 testdir v1", + path: "../../test/data/testdir_v1.torrent", + }, + { + name: "005 testdir v2", + path: "../../test/data/testdir_v2.torrent", + }, + { + name: "006 single hybryd", + path: "../../test/data/testfile1_single_hybrid.torrent", + }, + { + name: "007 single v1", + path: "../../test/data/testfile1_single_v1.torrent", + }, + { + name: "008 single v2", + path: "../../test/data/testfile1_single_v2.torrent", + }, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + var torrent Torrent + err := helpers.DecodeTorrentFile(testCase.path, &torrent) + if err != nil && !testCase.mustFail { + t.Fatalf("Unexpected error: %v", err) + } else if err == nil && testCase.mustFail { + t.Fatalf("Test must fail, but it doesn't") + } + }) + } +} + +func TestTorrent_GetFileList(t *testing.T) { + type PathJoinCase struct { + name string + path string + expected []string + mustFail bool + } + cases := []PathJoinCase{ + { + name: "001 testdir v2 mustfail", + path: "../../test/data/testdir_v2.torrent", + mustFail: true, + expected: []string{}, + }, + { + name: "002 testdir v2", + path: "../../test/data/testdir_v2.torrent", + expected: []string{ + "dir1/testfile1.txt", + "dir2/testfile1.txt", + "dir2/testfile2.txt", + "dir3/testfile1.txt", + "dir3/testfile2.txt", + "dir3/testfile3.txt", + "testfile1.txt", + "testfile2.txt", + "testfile3.txt", + }, + }, + { + name: "003 testdir v1", + path: "../../test/data/testdir_v1.torrent", + expected: []string{ + "testfile1.txt", + "testfile2.txt", + "testfile3.txt", + "dir1/testfile1.txt", + "dir2/testfile1.txt", + "dir2/testfile2.txt", + "dir3/testfile1.txt", + "dir3/testfile2.txt", + "dir3/testfile3.txt", + }, + }, + { + name: "004 testdir hybrid", + path: "../../test/data/testdir_hybrid.torrent", + expected: []string{ + "dir1/testfile1.txt", + "dir2/testfile1.txt", + "dir2/testfile2.txt", + "dir3/testfile1.txt", + "dir3/testfile2.txt", + "dir3/testfile3.txt", + "testfile1.txt", + "testfile2.txt", + "testfile3.txt", + }, + }, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + var torrent Torrent + err := helpers.DecodeTorrentFile(testCase.path, &torrent) + if err != nil { + t.Fatalf("Unexpected error with decoding torrent file: %v", err) + } + list := torrent.GetFileList() + equal := reflect.DeepEqual(list, testCase.expected) + if !equal && !testCase.mustFail { + changes, err := diff.Diff(list, testCase.expected, diff.DiscardComplexOrigin()) + if err != nil { + t.Error(err.Error()) + } + t.Fatalf("Unexpected error: opts isn't equal:\n Got: %#v\n Expect %#v\n Diff: %v\n", list, testCase.expected, spew.Sdump(changes)) + } else if equal && testCase.mustFail { + t.Fatalf("Unexpected error: structures are equal, but they shouldn't\n Got: %v\n", spew.Sdump(list)) + } + }) + } +} + +func TestTorrent_GetFileListWB(t *testing.T) { + type PathJoinCase struct { + name string + path string + expected []FilepathLength + mustFail bool + } + cases := []PathJoinCase{ + { + name: "001 testdir v2 mustfail", + path: "../../test/data/testdir_v2.torrent", + mustFail: true, + expected: []FilepathLength{}, + }, + { + name: "001 testdir v2", + path: "../../test/data/testdir_v2.torrent", + expected: []FilepathLength{ + FilepathLength{Path: "dir1/testfile1.txt", Length: 33}, + FilepathLength{Path: "dir2/testfile1.txt", Length: 33}, + FilepathLength{Path: "dir2/testfile2.txt", Length: 33}, + FilepathLength{Path: "dir3/testfile1.txt", Length: 33}, + FilepathLength{Path: "dir3/testfile2.txt", Length: 33}, + FilepathLength{Path: "dir3/testfile3.txt", Length: 33}, + FilepathLength{Path: "testfile1.txt", Length: 33}, + FilepathLength{Path: "testfile2.txt", Length: 33}, + FilepathLength{Path: "testfile3.txt", Length: 33}, + }, + }, + { + name: "003 testdir v1", + path: "../../test/data/testdir_v1.torrent", + expected: []FilepathLength{ + FilepathLength{Path: "testfile1.txt", Length: 33}, + FilepathLength{Path: "testfile2.txt", Length: 33}, + FilepathLength{Path: "testfile3.txt", Length: 33}, + FilepathLength{Path: "dir1/testfile1.txt", Length: 33}, + FilepathLength{Path: "dir2/testfile1.txt", Length: 33}, + FilepathLength{Path: "dir2/testfile2.txt", Length: 33}, + FilepathLength{Path: "dir3/testfile1.txt", Length: 33}, + FilepathLength{Path: "dir3/testfile2.txt", Length: 33}, + FilepathLength{Path: "dir3/testfile3.txt", Length: 33}, + }, + }, + { + name: "004 testdir hybrid", + path: "../../test/data/testdir_hybrid.torrent", + expected: []FilepathLength{ + FilepathLength{Path: "dir1/testfile1.txt", Length: 33}, + FilepathLength{Path: "dir2/testfile1.txt", Length: 33}, + FilepathLength{Path: "dir2/testfile2.txt", Length: 33}, + FilepathLength{Path: "dir3/testfile1.txt", Length: 33}, + FilepathLength{Path: "dir3/testfile2.txt", Length: 33}, + FilepathLength{Path: "dir3/testfile3.txt", Length: 33}, + FilepathLength{Path: "testfile1.txt", Length: 33}, + FilepathLength{Path: "testfile2.txt", Length: 33}, + FilepathLength{Path: "testfile3.txt", Length: 33}, + }, + }, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + var torrent Torrent + err := helpers.DecodeTorrentFile(testCase.path, &torrent) + if err != nil { + t.Fatalf("Unexpected error with decoding torrent file: %v", err) + } + list := torrent.GetFileListWB() + equal := reflect.DeepEqual(list, testCase.expected) + if !equal && !testCase.mustFail { + changes, err := diff.Diff(list, testCase.expected, diff.DiscardComplexOrigin()) + if err != nil { + t.Error(err.Error()) + } + t.Fatalf("Unexpected error: opts isn't equal:\n Got: %#v\n Expect %#v\n Diff: %v\n", list, testCase.expected, spew.Sdump(changes)) + } else if equal && testCase.mustFail { + t.Fatalf("Unexpected error: structures are equal, but they shouldn't\n Got: %v\n", spew.Sdump(list)) + } + }) + } +} diff --git a/pkg/torrentStructures/torrent.go b/pkg/torrentStructures/torrent.go index f60641f..8c5abd8 100644 --- a/pkg/torrentStructures/torrent.go +++ b/pkg/torrentStructures/torrent.go @@ -1,35 +1,32 @@ package torrentStructures type Torrent struct { - Announce string `bencode:"announce"` - Comment string `bencode:"comment"` - CreatedBy string `bencode:"created by"` - CreationDate int64 `bencode:"creation date"` - Info *TorrentInfo `bencode:"info"` - Publisher string `bencode:"publisher,omitempty"` - PublisherUrl string `bencode:"publisher-url,omitempty"` - PieceLayers *map[byte][]byte `bencode:"piece layers"` + Announce string `bencode:"announce"` + Comment string `bencode:"comment"` + CreatedBy string `bencode:"created by"` + CreationDate int64 `bencode:"creation date"` + Info *TorrentInfo `bencode:"info"` + Publisher string `bencode:"publisher,omitempty"` + PublisherUrl string `bencode:"publisher-url,omitempty"` + PieceLayers *map[string]interface{} `bencode:"piece layers"` + FilePathLength *[]FilepathLength `bencode:"-"` // service field + FilePaths *[]string `bencode:"-"` // service field } type TorrentInfo struct { - FileDuration []int64 `bencode:"file-duration,omitempty"` - FileMedia []int64 `bencode:"file-media,omitempty"` - Files []*TorrentFile `bencode:"files,omitempty"` - FileTree map[string]*[]TorrentFileTree `bencode:"file tree,omitempty"` - Length int64 `bencode:"length,omitempty"` - MetaVersion int64 `bencode:"meta version,omitempty"` - Md5sum string `bencode:"md5sum,omitempty"` - Name string `bencode:"name,omitempty"` - NameUTF8 string `bencode:"name.utf-8,omitempty"` - PieceLength int64 `bencode:"piece length,omitempty"` - Pieces []byte `bencode:"pieces,omitempty"` - Private uint8 `bencode:"private,omitempty"` - Profiles []*TorrentProfile `bencode:"profiles,omitempty"` -} - -type TorrentFileTree struct { - Length int64 `bencode:"length,omitempty"` - PiecesRoot []byte `bencode:"pieces root"` + FileDuration []int64 `bencode:"file-duration,omitempty"` + FileMedia []int64 `bencode:"file-media,omitempty"` + Files []*TorrentFile `bencode:"files,omitempty"` + FileTree map[string]interface{} `bencode:"file tree,omitempty"` + Length int64 `bencode:"length,omitempty"` + MetaVersion int64 `bencode:"meta version,omitempty"` + Md5sum string `bencode:"md5sum,omitempty"` + Name string `bencode:"name,omitempty"` + NameUTF8 string `bencode:"name.utf-8,omitempty"` + PieceLength int64 `bencode:"piece length,omitempty"` + Pieces []byte `bencode:"pieces,omitempty"` + Private uint8 `bencode:"private,omitempty"` + Profiles []*TorrentProfile `bencode:"profiles,omitempty"` } type TorrentFile struct { @@ -45,3 +42,8 @@ type TorrentProfile struct { Vcodec []byte `bencode:"vcodec,omitempty"` Width int64 `bencode:"width,omitempty"` } + +type FilepathLength struct { + Path string + Length int64 +} diff --git a/test/data/testdir_hybrid.fastresume b/test/data/testdir_hybrid.fastresume new file mode 100644 index 0000000000000000000000000000000000000000..95b7f3abd74a2ccd962392683e3a503a7ec02c6e GIT binary patch literal 1610 zcmeHHJ#Q015G5rNZ8{1lsF35^i+y&cLQo(iBrLufEFBx~*j}=?d)b{!;(q`gLP|$L zLr0BJ8hR+HD4_%iIs_fFcRnXUl$2a``(|$5%$v8TgaA1qy@xsJ0(F8S0jU(K<}9*) zB6?ZY+pGh|xQQg~YzCo}KA`A>EK9Sr3~(KMp^b z(a1n~J4u{t*g(u_^H#A&fI77*%l8ydG!dofi0T-|qdAZI8Es|Q- z2scH7?nM=BRU8wEY%mz4q96Aa18rwheulnYnU5N>%r(^eXh)zP-mk)(M7 zX#-BR5Bx!(Q6w38?YyQ1H#e-mv9dre->VO7BJFNACQ6g2Rnte1dl-xM25=3!_X3a7 z^%Wbtd)*mn)yTU?EOF>=Q@XoNUBW9oM|$ns`P_#_v}KMd{eHp(@em%c>E|fE=BuRU3fEZ_vhVH8uqDaAemPXB9%Pv94gL=^u>b%7 literal 0 HcmV?d00001 diff --git a/test/data/testdir_hybrid.torrent b/test/data/testdir_hybrid.torrent new file mode 100644 index 0000000000000000000000000000000000000000..66c494b3066f936a62ad4b35c1022f945d47af3e GIT binary patch literal 1883 zcmdUwKTE?v7{*1NI*5y)xI`BPCFXKTtT(!JaPtEQvG>*6X(ODSU{PpLd5|H`wUFa^4hDy?uolrRQey!af&);8P{t9Jw*fcY zxQxY5Opu3NBwe51Gn+njNmhhk(Mev%l2;5mm0uqk7uU6lF0kv=JX!=PL<$9}PXq;; zj}OQu4G_N30v^ka!EEW@&~LzIYRY6a*x8TY$ajJCbJ+ne-v+2`k<>U2qXE&KU-DgJNcjV|4;2U`9&LIE* literal 0 HcmV?d00001 diff --git a/test/data/testdir_v1.fastresume b/test/data/testdir_v1.fastresume new file mode 100644 index 0000000000000000000000000000000000000000..8180dba27ccd2fba78ede4da53a8ff9b325d3215 GIT binary patch literal 1191 zcmah|&2G~`5T+LpPr$X}o@UogoOF(GKuDE{#Lp?hn#7ZM>3Y}Q9k-3dkyqi)nH$f* zWAFx?n6=}CR!I2d{bqK)Z@!r=wFX?XxuJqhO-7McfMrQ#f7ba_=}Dfec^9zOuaS*+ zIm8A-Bs~Re8oMh@Q*4Z73PzL809*D&jSja|CS(F@UpJQcUxhW4CgRFcQC6HJ90424 z`#?i?3AljL-#Y8DECMnW*3^d0NSHK;4jOHIsFAY_tXZ)SfgGO(ifv7&DJ~Mbv>OUh z+DQ`61#u=;gF;0YP%$@=sEk$)#%=tl!dPVrCLg~4c(Q)oe0#qAeD(Y5m&YHUJ`?AT zDd!nH`p+kf79$r!C#kP_4+*C$2FH2>y41EwE$pPz3OGkH zT<@aMs`E9$eUWG;$pzQeMtN{3Ss{Yl<39M9TFG9?5pm)`y(}J?jDQSY~&LJKypg-UPEX4z13+=pb-` zTq($UaAQK03y~M8_#zk|4DUqLyu0JDCMiGZ4zfJPU-rwBspL_wBjOFEqLUfCgz@_* zEsR4e3q5}2ExK|M{3o$Cj#r#ruXxN9d0Zmncwabe&09up#d?y3NzEn9xa~W6XanV5 z6XUj9u|!)m1MjlqU3R?7`@GAZcbRyXiFbJcseDoUlKLmOXFI&>lyKdOmj>-&vT2nR z#0mrTSLHl~$i69VE7`$7SSIUaS7X~yDE3tolmmPIt5!F7X}?GNE$gs3aKQDIcqk7~ WLmxTN%?9B9L!Lt$?8utPmgo;$KbUU- literal 0 HcmV?d00001 diff --git a/test/data/testdir_v1.torrent b/test/data/testdir_v1.torrent new file mode 100644 index 0000000..258ec8a --- /dev/null +++ b/test/data/testdir_v1.torrent @@ -0,0 +1 @@ +d10:created by18:qBittorrent v4.4.213:creation datei1650146608e4:infod5:filesld6:lengthi33e4:pathl13:testfile1.txteed6:lengthi33e4:pathl13:testfile2.txteed6:lengthi33e4:pathl13:testfile3.txteed6:lengthi33e4:pathl4:dir113:testfile1.txteed6:lengthi33e4:pathl4:dir213:testfile1.txteed6:lengthi33e4:pathl4:dir213:testfile2.txteed6:lengthi33e4:pathl4:dir313:testfile1.txteed6:lengthi33e4:pathl4:dir313:testfile2.txteed6:lengthi33e4:pathl4:dir313:testfile3.txteee4:name7:testdir12:piece lengthi16384e6:pieces20:ã9Ép¶ç õAOͯw»såÕ64ee \ No newline at end of file diff --git a/test/data/testdir_v2.fastresume b/test/data/testdir_v2.fastresume new file mode 100644 index 0000000000000000000000000000000000000000..2e2c1e33b1f591c230dcb4b1f3428ba15d64b4f3 GIT binary patch literal 1622 zcmeHHJ&)5s5alE!L_@A%yC8a4CJtyZqTH5y* z>Js;3oLg+tPSFP*C$@OVh}B`;b(r7;)?x;0UnnbPIK7u^OSadjoXE1mZc$9&!+1P^@&Pc1K-o0gs!(dM7$(#m+ zfd)#F9Bd0m_q-xWHLn2KIB~d$X}fyO7p|k+_C#{kSS`{PRmVGPcxMgo>?-f9<();| zS>&BfAeJvmr?9<>TejW(jtM6a>fX8pq)@=NllVJr7r1l9r~|J4cgIkB(=$4dLHr8{SSr^5 literal 0 HcmV?d00001 diff --git a/test/data/testdir_v2.torrent b/test/data/testdir_v2.torrent new file mode 100644 index 0000000..60eb9b3 --- /dev/null +++ b/test/data/testdir_v2.torrent @@ -0,0 +1,10 @@ +d10:created by18:qBittorrent v4.4.213:creation datei1650146556e4:infod9:file treed4:dir1d13:testfile1.txtd0:d6:lengthi33e11:pieces root32:à1 +x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Öneee4:dir2d13:testfile1.txtd0:d6:lengthi33e11:pieces root32:à1 +x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Önee13:testfile2.txtd0:d6:lengthi33e11:pieces root32:à1 +x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Öneee4:dir3d13:testfile1.txtd0:d6:lengthi33e11:pieces root32:à1 +x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Önee13:testfile2.txtd0:d6:lengthi33e11:pieces root32:à1 +x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Önee13:testfile3.txtd0:d6:lengthi33e11:pieces root32:à1 +x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Öneee13:testfile1.txtd0:d6:lengthi33e11:pieces root32:à1 +x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Önee13:testfile2.txtd0:d6:lengthi33e11:pieces root32:à1 +x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Önee13:testfile3.txtd0:d6:lengthi33e11:pieces root32:à1 +x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Öneee12:meta versioni2e4:name7:testdir12:piece lengthi16384ee12:piece layersdee \ No newline at end of file diff --git a/test/data/testfile1_single_hybrid.fastresume b/test/data/testfile1_single_hybrid.fastresume new file mode 100644 index 0000000000000000000000000000000000000000..99c88a9cf194a1424a6f0ab419cd7f5630d84571 GIT binary patch literal 1250 zcmZuxJ#Q015GA4lAqpy_H2DLLZ!h-Qo(AzDXd+=Cbe2wQ@7UgCZ}+l0m&7F!B^5sb zDP;<%5f$POP|zb$K|x1@6lU*ihag;7yKi>qy_tD?N(hh>(npw+E>I^Z5|B!vYW5=Q zCZfZCJ9WSqH<6^B%^;N02NYe9WoZ_ktr%yC^|2%Lfy;>!cNnoc%vTB%9Kf8Y42UWi-e@~@t+@>IGy#8p@49&L;o6;V zZ$555y}fw&^Xx8n?&8Q!3a_rdJN@zH#@F}m_x|hiU#$mwH{T9U_iueVYdtwGUT!j@ zk%976C9$r412Lz~R~>5_P#>+zSn~4Vy0F%=iXNof$#!=;;i(kzgcDecJX~jk7D?UL z2scTB?!+$Gs<->p5&5s{m$co!-{*WrCCop zS}TDtmmZs|yzfFFSLVla_M}ItVk_CZp(@em%d8Q~fE@jmt2P3+p+Oth)mR_M%gF)v z)UZ*=BL<@~h))aIG95N=I1rHZfsL)F8kGQn@o6_n*2;P-;PN-150y79qXQYlKQ76> AasU7T literal 0 HcmV?d00001 diff --git a/test/data/testfile1_single_hybrid.torrent b/test/data/testfile1_single_hybrid.torrent new file mode 100644 index 0000000000000000000000000000000000000000..b423bdfdd0a51bd11a837ad60c672ea4bb2e810b GIT binary patch literal 290 zcmYc>G_Xo8N=+tDMxl^pcEB zW8+jqL#u+!)a2A+g`)iY5@RE)2Zme~oBp&MHa_Wo{=#{_eQONf zwY=2SR22P2R=KGqi3(+@Ma4iTWf}n;k(Zd8if*r=5yTz^sMUsM#ug?(lOUEG8CWqW iNi5iQlY!;uwW0;m)#|@LP4*5-Md(Vb1X_@inhF4UA8FM9 literal 0 HcmV?d00001 diff --git a/test/data/testfile1_single_v1.fastresume b/test/data/testfile1_single_v1.fastresume new file mode 100644 index 0000000000000000000000000000000000000000..c8fff4edcaf0c085ebf31cd651473852efcf133e GIT binary patch literal 1188 zcmah|OO6vU5QQzLmtTE%$1?xpupvo5%@^^C@z=dq$|7~&*AelKQqtKJ-oWI26jw|_C;pvWd5cc3ga0I= z#mSb-+bvI+B2P3j=94yI$e(O+!^oXzOX7&sW5SF(yp#1hF%P9N9G_Xo8N=+1y@gpC)?;rKSP^ddD{+ literal 0 HcmV?d00001 diff --git a/test/data/testfile1_single_v2.fastresume b/test/data/testfile1_single_v2.fastresume new file mode 100644 index 0000000000000000000000000000000000000000..9d22c62bc2aa4ba74489ddbf583d23c0c10f1d63 GIT binary patch literal 1262 zcmZux&2AGh5N;)Sgd&c8fh6l~c9YH#4qPe`sc;KgIq@WG>b1A_Y}@bxJOD?Ia6r65 zkB9?jBpw1zJOC2o-E8`!=45{}JKr}mpQD5Txg>po1?dWPf+7K_6sqnn@+=YKe4M9k zz!!OHncQksI%3v%A0r9+RQsqiqy zSz?=Zfj)3MQQ{FJR)?}znBXy#M0G$^!SF@fl^c{YlxYHc5Bzv?Hz`Lc92D1o-#z-Y z_vGp4_ru4(R^JYOd^r97``;J3{&-n^eZ!1K1}geaVqen^VoqE16YB=hrZ#nKd7HRv zY_RQO0_iXrX2XQHQpqbWU@h`;oef$f?OY?=6$yGAr(m1vhDhX-$s`q7+*b_r%Hj_1 zg|ywt+l6wdWfM3#!3=8T(I&uHJmlQjy^-zz{KG+wOAG?`S<3k{8nC-??ffPIG3fwj ziFbD*H?E;6y!3jltu!KUqrJeHq%wiD16SGy{vgmS7N2?TyrvC{nzr9qSs|Anw-+{# zb|;;A(j?BP(-Gta#-dpU&tddBa0?^fu%SmUoRL<|ynD-|4Wk96=L;GU1{x_zvOOv7 zM_YpEo>P+4@CuNP6Pt{fw(D2?;CkwW8vL`)C6-UY54NZ-{ z*j9~D4&>-ot=0%Uh7Rpq*J4v3FPCRLQ^Q6fj~I-~AwDf+%Z%B);Y>g>1va)`X;cCP b-pauE%)5d+m6IO0`+qQn#+!lBfehk*1v9x$ literal 0 HcmV?d00001 diff --git a/test/data/testfile1_single_v2.torrent b/test/data/testfile1_single_v2.torrent new file mode 100644 index 0000000..9ff799d --- /dev/null +++ b/test/data/testfile1_single_v2.torrent @@ -0,0 +1,2 @@ +d10:created by18:qBittorrent v4.4.213:creation datei1650146462e4:infod9:file treed13:testfile1.txtd0:d6:lengthi33e11:pieces root32:à1 +x²ü„Ã3ÉGÏÐϾ¬0þ(2üËÁ,/Öneee12:meta versioni2e4:name13:testfile1.txt12:piece lengthi16384ee12:piece layersdee \ No newline at end of file