+++ /dev/null
-package torrent
-
-import (
- "errors"
- "fmt"
- "io"
- "os"
- "path/filepath"
- "runtime"
- "testing"
-
- "github.com/anacrolix/torrent/dht"
- "github.com/anacrolix/torrent/metainfo"
-)
-
-var numclients int = 0
-
-func cfdir() string {
- numclients++
- return filepath.Join(os.TempDir(), "wtp-test/", fmt.Sprintf("%d/", numclients))
-}
-
-func addirs(cf *Config) *Config {
- d := cfdir()
- os.MkdirAll(d, 0700)
- cf.DataDir = filepath.Join(d, "/data")
- os.MkdirAll(cf.DataDir, 0700)
- cf.ConfigDir = filepath.Join(d, "/config")
- os.MkdirAll(cf.ConfigDir, 0700)
- return cf
-}
-
-func issue35TestingConfig() *Config {
- return &Config{
- ListenAddr: "localhost:0",
- NoDHT: false,
- DisableTrackers: true,
- DisableUTP: false,
- DisableMetainfoCache: true,
- DisableIPv6: true,
- NoUpload: false,
- Seed: true,
- DataDir: filepath.Join(os.TempDir(), "torrent-test/data"),
- ConfigDir: filepath.Join(os.TempDir(), "torrent-test/config"),
- DHTConfig: dht.ServerConfig{
- Passive: false,
- BootstrapNodes: []string{},
- NoSecurity: false,
- NoDefaultBootstrap: true,
- },
- Debug: true,
- }
-}
-
-func writeranddata(path string) error {
- var w int64
- var to_write int64 = 1024 * 1024 //1MB
- f, err := os.Create(path)
- defer f.Close()
- if err != nil {
- return err
- }
- rnd, err := os.Open("/dev/urandom")
- defer rnd.Close()
- if err != nil {
- return err
- }
- w, err = io.CopyN(f, rnd, to_write)
- if err != nil {
- return err
- }
- if w != to_write {
- return errors.New("Short read on /dev/random")
- }
- return nil
-}
-
-func TestInfohash(t *testing.T) {
- os.RemoveAll(filepath.Join(os.TempDir(), "torrent-test"))
- os.MkdirAll(filepath.Join(os.TempDir(), "torrent-test"), 0700)
- var cl_one *Client
- var cf_one *Config
- var err error
- if err != nil {
- t.Fatal(err)
- }
- cf_one = issue35TestingConfig()
- cf_one.ListenAddr = "localhost:43433"
- cf_one = addirs(cf_one)
- cl_one, err = NewClient(cf_one)
- if err != nil {
- t.Fatal(err)
- }
- tfp := filepath.Join(cf_one.DataDir, "testdata")
- writeranddata(tfp)
- b := metainfo.Builder{}
- b.AddFile(tfp)
- b.AddDhtNodes([]string{"1.2.3.4:5555"})
- ba, err := b.Submit()
- if err != nil {
- t.Fatal(err)
- }
- ttfp := filepath.Join(cf_one.ConfigDir, "/../torrent")
- ttf, err := os.Create(ttfp)
- if err != nil {
- t.Fatal(err)
- }
- ec, _ := ba.Start(ttf, runtime.NumCPU())
- err = <-ec
- if err != nil {
- t.Fatal(err)
- }
- ttf.Close()
-
- tor, err := cl_one.AddTorrentFromFile(ttfp)
- if err != nil {
- t.Fatal(err)
- }
- <-tor.GotInfo()
- tor.DownloadAll()
- if cl_one.WaitAll() == false {
- t.Fatal(errors.New("One did not download torrent"))
- }
-}
+++ /dev/null
-package metainfo
-
-import (
- "crypto/sha1"
- "errors"
- "hash"
- "io"
- "os"
- "path/filepath"
- "sort"
- "time"
-
- "github.com/anacrolix/missinggo"
-
- "github.com/anacrolix/torrent/bencode"
-)
-
-//----------------------------------------------------------------------------
-// Build
-//----------------------------------------------------------------------------
-
-// The Builder type is responsible for .torrent files construction. Just
-// instantiate it, call necessary methods and then call the .Build method. While
-// waiting for completion you can use 'status' channel to get status reports.
-type Builder struct {
- batch_state
- filesmap map[string]bool
-}
-
-// Adds a file to the builder queue. You may add one or more files.
-func (b *Builder) AddFile(filename string) {
- if b.filesmap == nil {
- b.filesmap = make(map[string]bool)
- }
-
- filename, err := filepath.Abs(filename)
- if err != nil {
- panic(err)
- }
- b.filesmap[filename] = true
-}
-
-// Defines a name of the future torrent file. For single file torrents it's the
-// recommended name of the contained file. For multiple files torrents it's the
-// recommended name of the directory in which all of them will be
-// stored. Calling this function is not required. In case if no name was
-// specified, the builder will try to automatically assign it. It will use the
-// name of the file if there is only one file in the queue or it will try to
-// find the rightmost common directory of all the queued files and use its name as
-// a torrent name. In case if name cannot be assigned automatically, it will use
-// "unknown" as a torrent name.
-func (b *Builder) SetName(name string) {
- b.name = name
-}
-
-// Sets the length of a piece in the torrent file in bytes. The default is
-// 256kb.
-func (b *Builder) SetPieceLength(length int64) {
- b.piece_length = length
-}
-
-// Sets the "private" flag. The default is false.
-func (b *Builder) SetPrivate(v bool) {
- b.private = v
-}
-
-// Add announce URL group. TODO: better explanation.
-func (b *Builder) AddAnnounceGroup(group []string) {
- b.announce_list = append(b.announce_list, group)
-}
-
-// Add DHT nodes URLs for trackerless mode
-func (b *Builder) AddDhtNodes(group []string) {
- b.node_list = append(b.node_list, group...)
-}
-
-// Sets creation date. The default is time.Now() when the .Build method was
-// called.
-func (b *Builder) SetCreationDate(date time.Time) {
- b.creation_date = date
-}
-
-// Sets the comment. The default is no comment.
-func (b *Builder) SetComment(comment string) {
- b.comment = comment
-}
-
-// Sets the "created by" parameter. The default is "libtorgo".
-func (b *Builder) SetCreatedBy(createdby string) {
- b.created_by = createdby
-}
-
-// Sets the "encoding" parameter. The default is "UTF-8".
-func (b *Builder) SetEncoding(encoding string) {
- b.encoding = encoding
-}
-
-// Add WebSeed URL to the list.
-func (b *Builder) AddWebSeedURL(url string) {
- b.urls = append(b.urls, url)
-}
-
-// Finalizes the Builder state and makes a Batch out of it. After calling that
-// method, Builder becomes empty and you can use it to create another Batch if
-// you will.
-func (b *Builder) Submit() (*Batch, error) {
- err := b.check_parameters()
- if err != nil {
- return nil, err
- }
- b.set_defaults()
-
- batch := &Batch{
- batch_state: b.batch_state,
- }
-
- const non_regular = os.ModeDir | os.ModeSymlink |
- os.ModeDevice | os.ModeNamedPipe | os.ModeSocket
-
- // convert a map to a slice, calculate sizes and split paths
- batch.total_size = 0
- batch.files = make([]file, 0, 10)
- for f, _ := range b.filesmap {
- var file file
- fi, err := os.Stat(f)
- if err != nil {
- return nil, err
- }
-
- if fi.Mode()&non_regular != 0 {
- return nil, errors.New(f + " is not a regular file")
- }
-
- file.abspath = f
- file.splitpath = split_path(f)
- file.size = fi.Size()
- batch.files = append(batch.files, file)
- batch.total_size += file.size
- }
-
- // find the rightmost common directory
- if len(batch.files) == 1 {
- sp := batch.files[0].splitpath
- batch.default_name = sp[len(sp)-1]
- } else {
- common := batch.files[0].splitpath
- for _, f := range batch.files {
- if len(common) > len(f.splitpath) {
- common = common[:len(f.splitpath)]
- }
-
- for i, n := 0, len(common); i < n; i++ {
- if common[i] != f.splitpath[i] {
- common = common[:i]
- break
- }
- }
-
- if len(common) == 0 {
- break
- }
- }
-
- if len(common) == 0 {
- return nil, errors.New("no common rightmost folder was found for a set of queued files")
- }
-
- // found the common folder, let's strip that part from splitpath
- // and setup the default name
- batch.default_name = common[len(common)-1]
-
- lcommon := len(common)
- for i := range batch.files {
- f := &batch.files[i]
- f.splitpath = f.splitpath[lcommon:]
- }
-
- // and finally sort the files
- sort.Sort(file_slice(batch.files))
- }
-
- // reset the builder state
- b.batch_state = batch_state{}
- b.filesmap = nil
-
- return batch, nil
-}
-
-func (b *Builder) set_defaults() {
- if b.piece_length == 0 {
- b.piece_length = 256 * 1024
- }
-
- if b.creation_date.IsZero() {
- b.creation_date = time.Now()
- }
-
- if b.created_by == "" {
- b.created_by = "libtorgo"
- }
-
- if b.encoding == "" {
- b.encoding = "UTF-8"
- }
-}
-
-func emptyStringsFiltered(ss []string) (ret []string) {
- for _, s := range ss {
- if s != "" {
- ret = append(ret, s)
- }
- }
- return
-}
-
-func (b *Builder) check_parameters() error {
- // should be at least one file
- if len(b.filesmap) == 0 {
- return errors.New("no files were queued")
- }
-
- // let's clean up the announce_list and node_list
- b.announce_list = cleanUpLists(b.announce_list)
- b.node_list = emptyStringsFiltered(b.node_list)
-
- if len(b.announce_list) == 0 && len(b.node_list) == 0 {
- return errors.New("no announce group or DHT nodes specified")
- }
-
- // Either the node_list or announce_list can be present
- // Never the both!
- if len(b.announce_list) > 0 && len(b.node_list) > 0 {
- return errors.New("announce group and nodes are mutually exclusive")
- }
-
- // and clean up the urls
- b.urls = remove_empty_strings(b.urls)
-
- return nil
-}
-
-func cleanUpLists(list [][]string) [][]string {
- newList := make([][]string, 0, len(list))
- for _, l := range list {
- l = remove_empty_strings(l)
-
- // discard empty announce groups
- if len(l) == 0 {
- continue
- }
- newList = append(newList, l)
- }
- return newList
-}
-
-//----------------------------------------------------------------------------
-// Batch
-//----------------------------------------------------------------------------
-
-// Batch represents a snapshot of a builder state, ready for transforming it
-// into a torrent file. Note that Batch contains two accessor methods you might
-// be interested in. The TotalSize is the total size of all the files queued for
-// hashing, you will use it for status reporting. The DefaultName is an
-// automatically determined name of the torrent metainfo, you might want to use
-// it for naming the .torrent file itself.
-type Batch struct {
- batch_state
- files []file
- total_size int64
- default_name string
-}
-
-// Get a total size of all the files queued for hashing. Useful in conjunction
-// with status reports.
-func (b *Batch) TotalSize() int64 {
- return b.total_size
-}
-
-// Get an automatically determined name of the future torrent metainfo. You can
-// use it for a .torrent file in case user hasn't provided it specifically.
-func (b *Batch) DefaultName() string {
- return b.default_name
-}
-
-// Starts a process of building the torrent file. This function does everything
-// in a separate goroutine and uses up to 'nworkers' of goroutines to perform
-// SHA1 hashing. Therefore it will return almost immedately. It returns two
-// channels, the first one is for completion awaiting, the second one is for
-// getting status reports. Status report is a number of bytes hashed, you can
-// get the total amount of bytes by inspecting the Batch.TotalSize method return
-// value.
-func (b *Batch) Start(w io.Writer, nworkers int) (<-chan error, <-chan int64) {
- if nworkers <= 0 {
- nworkers = 1
- }
-
- completion := make(chan error)
- status := make(chan int64)
-
- go func() {
- // prepare workers
- workers := make([]*worker, nworkers)
- free_workers := make(chan *worker, nworkers)
- for i := 0; i < nworkers; i++ {
- workers[i] = new_worker(free_workers)
- }
- stop_workers := func() {
- for _, w := range workers {
- w.stop()
- }
- for _, w := range workers {
- w.wait_for_stop()
- }
- }
-
- // prepare files for reading
- fr := files_reader{files: b.files}
- npieces := (b.total_size + b.piece_length - 1) / b.piece_length
- b.pieces = make([]byte, 20*npieces)
- hashed := int64(0)
-
- // read all the pieces passing them to workers for hashing
- var data []byte
- for i := int64(0); i < npieces; i++ {
- if data == nil {
- data = make([]byte, b.piece_length)
- }
-
- nr, err := fr.Read(data)
- if err != nil {
- // EOF is not an eror if it was the last piece
- if err == io.EOF {
- if i != npieces-1 {
- stop_workers()
- completion <- err
- return
- }
- } else {
- stop_workers()
- completion <- err
- return
- }
- }
-
- // cut the data slice to the amount of actual data read
- data = data[:nr]
- w := <-free_workers
- data = w.queue(data, b.pieces[20*i:20*i+20])
-
- // update and try to send the status report
- if data != nil {
- hashed += int64(len(data))
- data = data[:cap(data)]
-
- select {
- case status <- hashed:
- default:
- }
- }
- }
- stop_workers()
-
- // at this point the hash was calculated and we're ready to
- // write the torrent file
- err := b.write_torrent(w)
- if err != nil {
- completion <- err
- return
- }
- completion <- nil
- }()
- return completion, status
-}
-
-func (b *Batch) write_torrent(w io.Writer) error {
- var td MetaInfo
-
- // Either announce or node lists are allowed - not both
- if len(b.announce_list) != 0 {
- td.Announce = b.announce_list[0][0]
- if len(b.announce_list) != 1 || len(b.announce_list[0]) != 1 {
- td.AnnounceList = b.announce_list
- }
- }
-
- missinggo.CastSlice(&td.Nodes, b.node_list)
- td.CreationDate = b.creation_date.Unix()
- td.Comment = b.comment
- td.CreatedBy = b.created_by
- td.Encoding = b.encoding
- switch {
- case len(b.urls) == 0:
- case len(b.urls) == 1:
- td.URLList = b.urls[0]
- default:
- td.URLList = b.urls
- }
-
- td.Info.PieceLength = b.piece_length
- td.Info.Pieces = b.pieces
- if b.name == "" {
- td.Info.Name = b.default_name
- } else {
- td.Info.Name = b.name
- }
- if len(b.files) == 1 {
- td.Info.Length = b.files[0].size
- } else {
- td.Info.Files = make([]FileInfo, len(b.files))
- for i, f := range b.files {
- td.Info.Files[i] = FileInfo{
- Path: f.splitpath,
- Length: f.size,
- }
- }
- }
- td.Info.Private = b.private
-
- e := bencode.NewEncoder(w)
- return e.Encode(&td)
-}
-
-//----------------------------------------------------------------------------
-// misc stuff
-//----------------------------------------------------------------------------
-
-// splits path into components (dirs and files), works only on absolute paths
-func split_path(path string) []string {
- var dir, file string
- s := make([]string, 0, 5)
-
- dir = path
- for {
- dir, file = filepath.Split(filepath.Clean(dir))
- if file == "" {
- break
- }
- s = append(s, file)
- }
-
- // reverse the slice
- for i, n := 0, len(s)/2; i < n; i++ {
- i2 := len(s) - i - 1
- s[i], s[i2] = s[i2], s[i]
- }
-
- return s
-}
-
-// just a common data between the Builder and the Batch
-type batch_state struct {
- name string
- piece_length int64
- pieces []byte
- private bool
- announce_list [][]string
- node_list []string
- creation_date time.Time
- comment string
- created_by string
- encoding string
- urls []string
-}
-
-type file struct {
- abspath string
- splitpath []string
- size int64
-}
-
-type file_slice []file
-
-func (s file_slice) Len() int { return len(s) }
-func (s file_slice) Less(i, j int) bool { return s[i].abspath < s[j].abspath }
-func (s file_slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
-
-func remove_empty_strings(slice []string) []string {
- j := 0
- for i, n := 0, len(slice); i < n; i++ {
- if slice[i] == "" {
- continue
- }
- slice[j] = slice[i]
- j++
- }
- return slice[:j]
-}
-
-//----------------------------------------------------------------------------
-// worker
-//----------------------------------------------------------------------------
-
-type worker struct {
- msgbox chan bool
- hash hash.Hash
-
- // request
- sha1 []byte
- data []byte
-}
-
-// returns existing 'data'
-func (w *worker) queue(data, sha1 []byte) []byte {
- d := w.data
- w.data = data
- w.sha1 = sha1
- w.msgbox <- false
- return d
-}
-
-func (w *worker) stop() {
- w.msgbox <- true
-}
-
-func (w *worker) wait_for_stop() {
- <-w.msgbox
-}
-
-func new_worker(out chan<- *worker) *worker {
- w := &worker{
- msgbox: make(chan bool),
- hash: sha1.New(),
- }
- go func() {
- var sha1 [20]byte
- for {
- if <-w.msgbox {
- w.msgbox <- true
- return
- }
- w.hash.Reset()
- w.hash.Write(w.data)
- w.hash.Sum(sha1[:0])
- copy(w.sha1, sha1[:])
- out <- w
- }
- }()
- out <- w
- return w
-}
-
-//----------------------------------------------------------------------------
-// files_reader
-//----------------------------------------------------------------------------
-
-type files_reader struct {
- files []file
- cur int
- curfile *os.File
- off int64
-}
-
-func (f *files_reader) Read(data []byte) (int, error) {
- if f.cur >= len(f.files) {
- return 0, io.EOF
- }
-
- if len(data) == 0 {
- return 0, nil
- }
-
- read := 0
- for len(data) > 0 {
- file := &f.files[f.cur]
- if f.curfile == nil {
- var err error
- f.curfile, err = os.Open(file.abspath)
- if err != nil {
- return read, err
- }
- }
-
- // we need to read up to 'len(data)' bytes from current file
- n := int64(len(data))
-
- // unless there is not enough data in this file
- if file.size-f.off < n {
- n = file.size - f.off
- }
-
- // if there is no data in this file, try next one
- if n == 0 {
- err := f.curfile.Close()
- if err != nil {
- return read, err
- }
-
- f.curfile = nil
- f.off = 0
- f.cur++
- if f.cur >= len(f.files) {
- return read, io.EOF
- }
- continue
- }
-
- // read, handle errors
- nr, err := f.curfile.Read(data[:n])
- read += nr
- f.off += int64(nr)
- if err != nil {
- return read, err
- }
-
- // ok, we've read nr bytes out of len(data), cut the data slice
- data = data[nr:]
- }
-
- return read, nil
-}