The existing builder API is gross and heavy-handed. I won't rip it out just yet.
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 (
}
)
-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)
}
}
// 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,
import (
"crypto/sha1"
+ "errors"
+ "fmt"
"io"
+ "log"
"os"
+ "path/filepath"
+ "strings"
+ "time"
"github.com/anacrolix/torrent/bencode"
)
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 {
}
func (me *Info) NumPieces() int {
+ if len(me.Pieces)%20 != 0 {
+ panic(len(me.Pieces))
+ }
return len(me.Pieces) / 20
}
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
+}
import (
"bytes"
+ "io"
+ "io/ioutil"
"path"
"testing"
+ "github.com/anacrolix/missinggo"
+ "github.com/stretchr/testify/assert"
+
"github.com/anacrolix/torrent/bencode"
)
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())
+ }
+}