]> Sergey Matveev's repositories - btrtrc.git/commitdiff
metainfo: Add alternative "builder" API
authorMatt Joiner <anacrolix@gmail.com>
Thu, 29 Oct 2015 14:21:09 +0000 (01:21 +1100)
committerMatt Joiner <anacrolix@gmail.com>
Thu, 29 Oct 2015 14:21:09 +0000 (01:21 +1100)
The existing builder API is gross and heavy-handed. I won't rip it out just yet.

cmd/torrent-create/main.go
internal/testutil/testutil.go
metainfo/metainfo.go
metainfo/metainfo_test.go

index 0a877c25bd03c76d56e115bfe07b2097e5324a27..9174c85798d4a02c9b11ecb35c97bad3b7ff17dc 100644 (file)
@@ -1,13 +1,15 @@
 package main
 
 import (
-       "flag"
+       "io"
        "log"
        "os"
        "path/filepath"
-       "runtime"
+       "strings"
 
-       torrent "github.com/anacrolix/torrent/metainfo"
+       "github.com/docopt/docopt-go"
+
+       "github.com/anacrolix/torrent/metainfo"
 )
 
 var (
@@ -18,50 +20,28 @@ var (
        }
 )
 
-func init() {
-       flag.Parse()
-       runtime.GOMAXPROCS(runtime.NumCPU())
-}
-
 func main() {
-       b := torrent.Builder{}
-       for _, filename := range flag.Args() {
-               if err := filepath.Walk(filename, func(path string, info os.FileInfo, err error) error {
-                       if _, err := os.Stat(path); os.IsNotExist(err) {
-                               return err
-                       }
-                       log.Print(path)
-                       if info.IsDir() {
-                               return nil
-                       }
-                       b.AddFile(path)
-                       return nil
-               }); err != nil {
-                       log.Print(err)
-               }
+       opts, err := docopt.Parse("Usage: torrent-create <root>", nil, true, "", true)
+       if err != nil {
+               panic(err)
        }
-       for _, group := range builtinAnnounceList {
-               b.AddAnnounceGroup(group)
+       root := opts["<root>"].(string)
+       mi := metainfo.MetaInfo{
+               AnnounceList: builtinAnnounceList,
        }
-       batch, err := b.Submit()
+       mi.SetDefaults()
+       err = mi.Info.BuildFromFilePath(root)
        if err != nil {
                log.Fatal(err)
        }
-       errs, status := batch.Start(os.Stdout, runtime.NumCPU())
-       lastProgress := int64(-1)
-       for {
-               select {
-               case err, ok := <-errs:
-                       if !ok || err == nil {
-                               return
-                       }
-                       log.Print(err)
-               case bytesDone := <-status:
-                       progress := 100 * bytesDone / batch.TotalSize()
-                       if progress != lastProgress {
-                               log.Printf("%d%%", progress)
-                               lastProgress = progress
-                       }
-               }
+       err = mi.Info.GeneratePieces(func(fi metainfo.FileInfo) (io.ReadCloser, error) {
+               return os.Open(filepath.Join(root, strings.Join(fi.Path, string(filepath.Separator))))
+       })
+       if err != nil {
+               log.Fatalf("error generating pieces: %s", err)
+       }
+       err = mi.Write(os.Stdout)
+       if err != nil {
+               log.Fatal(err)
        }
 }
index 552f9a8eb39a555a6f7d3bed51a5dca80df823f1..b23ed5af107a4b2afce491b2f252f2f715193c8d 100644 (file)
@@ -26,16 +26,22 @@ func CreateDummyTorrentData(dirName string) string {
 
 // Writes to w, a metainfo containing the file at name.
 func CreateMetaInfo(name string, w io.Writer) {
-       builder := metainfo.Builder{}
-       builder.AddFile(name)
-       builder.AddAnnounceGroup([]string{"lol://cheezburger"})
-       builder.SetPieceLength(5)
-       batch, err := builder.Submit()
+       var mi metainfo.MetaInfo
+       mi.Info.Name = filepath.Base(name)
+       fi, _ := os.Stat(name)
+       mi.Info.Length = fi.Size()
+       mi.Announce = "lol://cheezburger"
+       mi.Info.PieceLength = 5
+       err := mi.Info.GeneratePieces(func(metainfo.FileInfo) (io.ReadCloser, error) {
+               return os.Open(name)
+       })
+       if err != nil {
+               panic(err)
+       }
+       err = mi.Write(w)
        if err != nil {
                panic(err)
        }
-       errs, _ := batch.Start(w, 1)
-       <-errs
 }
 
 // Gives a temporary directory containing the completed "greeting" torrent,
index 47fe7de6fa34b6325b8aec0337ebefbae9ed8521..deb936a203f28857c1b18ccf0c0395f5db3b18ec 100644 (file)
@@ -2,8 +2,14 @@ package metainfo
 
 import (
        "crypto/sha1"
+       "errors"
+       "fmt"
        "io"
+       "log"
        "os"
+       "path/filepath"
+       "strings"
+       "time"
 
        "github.com/anacrolix/torrent/bencode"
 )
@@ -46,6 +52,90 @@ type Info struct {
        Files       []FileInfo `bencode:"files,omitempty"`
 }
 
+func (info *Info) BuildFromFilePath(root string) (err error) {
+       info.Name = filepath.Base(root)
+       info.Files = nil
+       err = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
+               log.Println(path, root, err)
+               if fi.IsDir() {
+                       // Directories are implicit in torrent files.
+                       return nil
+               } else if path == root {
+                       // The root is a file.
+                       info.Length = fi.Size()
+                       return nil
+               }
+               relPath, err := filepath.Rel(root, path)
+               log.Println(relPath, err)
+               if err != nil {
+                       return fmt.Errorf("error getting relative path: %s", err)
+               }
+               info.Files = append(info.Files, FileInfo{
+                       Path:   strings.Split(relPath, string(filepath.Separator)),
+                       Length: fi.Size(),
+               })
+               return nil
+       })
+       if err != nil {
+               return
+       }
+       err = info.GeneratePieces(func(fi FileInfo) (io.ReadCloser, error) {
+               return os.Open(filepath.Join(root, strings.Join(fi.Path, string(filepath.Separator))))
+       })
+       if err != nil {
+               err = fmt.Errorf("error generating pieces: %s", err)
+       }
+       return
+}
+
+func (info *Info) writeFiles(w io.Writer, open func(fi FileInfo) (io.ReadCloser, error)) error {
+       for _, fi := range info.UpvertedFiles() {
+               r, err := open(fi)
+               if err != nil {
+                       return fmt.Errorf("error opening %s: %s", fi, err)
+               }
+               wn, err := io.CopyN(w, r, fi.Length)
+               r.Close()
+               if wn != fi.Length || err != nil {
+                       return fmt.Errorf("error hashing %s: %s", fi, err)
+               }
+       }
+       return nil
+}
+
+// Set info.Pieces by hashing info.Files.
+func (info *Info) GeneratePieces(open func(fi FileInfo) (io.ReadCloser, error)) error {
+       if info.PieceLength == 0 {
+               return errors.New("piece length must be non-zero")
+       }
+       pr, pw := io.Pipe()
+       go func() {
+               err := info.writeFiles(pw, open)
+               pw.CloseWithError(err)
+       }()
+       defer pr.Close()
+       var pieces []byte
+       for {
+               hasher := sha1.New()
+               wn, err := io.CopyN(hasher, pr, info.PieceLength)
+               if err == io.EOF {
+                       err = nil
+               }
+               if err != nil {
+                       return err
+               }
+               if wn == 0 {
+                       break
+               }
+               pieces = hasher.Sum(pieces)
+               if wn < info.PieceLength {
+                       break
+               }
+       }
+       info.Pieces = pieces
+       return nil
+}
+
 func (me *Info) TotalLength() (ret int64) {
        if me.IsDir() {
                for _, fi := range me.Files {
@@ -58,6 +148,9 @@ func (me *Info) TotalLength() (ret int64) {
 }
 
 func (me *Info) NumPieces() int {
+       if len(me.Pieces)%20 != 0 {
+               panic(len(me.Pieces))
+       }
        return len(me.Pieces) / 20
 }
 
@@ -147,3 +240,16 @@ type MetaInfo struct {
        Encoding     string      `bencode:"encoding,omitempty"`
        URLList      interface{} `bencode:"url-list,omitempty"`
 }
+
+// Encode to bencoded form.
+func (mi *MetaInfo) Write(w io.Writer) error {
+       return bencode.NewEncoder(w).Encode(mi)
+}
+
+// Set good default values in preparation for creating a new MetaInfo file.
+func (mi *MetaInfo) SetDefaults() {
+       mi.Comment = "yoloham"
+       mi.CreatedBy = "github.com/anacrolix/torrent"
+       mi.CreationDate = time.Now().Unix()
+       mi.Info.PieceLength = 256 * 1024
+}
index 16746c5bbdd91761075608b00740726b8663c7f8..2e6d8f08dd317811dc6473b2ec9a023b9cbff914 100644 (file)
@@ -2,9 +2,14 @@ package metainfo
 
 import (
        "bytes"
+       "io"
+       "io/ioutil"
        "path"
        "testing"
 
+       "github.com/anacrolix/missinggo"
+       "github.com/stretchr/testify/assert"
+
        "github.com/anacrolix/torrent/bencode"
 )
 
@@ -45,3 +50,28 @@ func TestFile(t *testing.T) {
        test_file(t, "_testdata/23516C72685E8DB0C8F15553382A927F185C4F01.torrent")
        test_file(t, "_testdata/trackerless.torrent")
 }
+
+// Ensure that the correct number of pieces are generated when hashing files.
+func TestNumPieces(t *testing.T) {
+       for _, _case := range []struct {
+               PieceLength int64
+               Files       []FileInfo
+               NumPieces   int
+       }{
+               {256 * 1024, []FileInfo{{Length: 1024*1024 + -1}}, 4},
+               {256 * 1024, []FileInfo{{Length: 1024 * 1024}}, 4},
+               {256 * 1024, []FileInfo{{Length: 1024*1024 + 1}}, 5},
+               {5, []FileInfo{{Length: 1}, {Length: 12}}, 3},
+               {5, []FileInfo{{Length: 4}, {Length: 12}}, 4},
+       } {
+               info := Info{
+                       Files:       _case.Files,
+                       PieceLength: _case.PieceLength,
+               }
+               err := info.GeneratePieces(func(fi FileInfo) (io.ReadCloser, error) {
+                       return ioutil.NopCloser(missinggo.ZeroReader{}), nil
+               })
+               assert.NoError(t, err)
+               assert.EqualValues(t, _case.NumPieces, info.NumPieces())
+       }
+}