13 "github.com/anacrolix/missinggo"
15 "github.com/anacrolix/torrent/bencode"
18 //----------------------------------------------------------------------------
20 //----------------------------------------------------------------------------
22 // The Builder type is responsible for .torrent files construction. Just
23 // instantiate it, call necessary methods and then call the .Build method. While
24 // waiting for completion you can use 'status' channel to get status reports.
27 filesmap map[string]bool
30 // Adds a file to the builder queue. You may add one or more files.
31 func (b *Builder) AddFile(filename string) {
32 if b.filesmap == nil {
33 b.filesmap = make(map[string]bool)
36 filename, err := filepath.Abs(filename)
40 b.filesmap[filename] = true
43 // Defines a name of the future torrent file. For single file torrents it's the
44 // recommended name of the contained file. For multiple files torrents it's the
45 // recommended name of the directory in which all of them will be
46 // stored. Calling this function is not required. In case if no name was
47 // specified, the builder will try to automatically assign it. It will use the
48 // name of the file if there is only one file in the queue or it will try to
49 // find the rightmost common directory of all the queued files and use its name as
50 // a torrent name. In case if name cannot be assigned automatically, it will use
51 // "unknown" as a torrent name.
52 func (b *Builder) SetName(name string) {
56 // Sets the length of a piece in the torrent file in bytes. The default is
58 func (b *Builder) SetPieceLength(length int64) {
59 b.piece_length = length
62 // Sets the "private" flag. The default is false.
63 func (b *Builder) SetPrivate(v bool) {
67 // Add announce URL group. TODO: better explanation.
68 func (b *Builder) AddAnnounceGroup(group []string) {
69 b.announce_list = append(b.announce_list, group)
72 // Add DHT nodes URLs for trackerless mode
73 func (b *Builder) AddDhtNodes(group []string) {
74 b.node_list = append(b.node_list, group...)
77 // Sets creation date. The default is time.Now() when the .Build method was
79 func (b *Builder) SetCreationDate(date time.Time) {
80 b.creation_date = date
83 // Sets the comment. The default is no comment.
84 func (b *Builder) SetComment(comment string) {
88 // Sets the "created by" parameter. The default is "libtorgo".
89 func (b *Builder) SetCreatedBy(createdby string) {
90 b.created_by = createdby
93 // Sets the "encoding" parameter. The default is "UTF-8".
94 func (b *Builder) SetEncoding(encoding string) {
98 // Add WebSeed URL to the list.
99 func (b *Builder) AddWebSeedURL(url string) {
100 b.urls = append(b.urls, url)
103 // Finalizes the Builder state and makes a Batch out of it. After calling that
104 // method, Builder becomes empty and you can use it to create another Batch if
106 func (b *Builder) Submit() (*Batch, error) {
107 err := b.check_parameters()
114 batch_state: b.batch_state,
117 const non_regular = os.ModeDir | os.ModeSymlink |
118 os.ModeDevice | os.ModeNamedPipe | os.ModeSocket
120 // convert a map to a slice, calculate sizes and split paths
122 batch.files = make([]file, 0, 10)
123 for f, _ := range b.filesmap {
125 fi, err := os.Stat(f)
130 if fi.Mode()&non_regular != 0 {
131 return nil, errors.New(f + " is not a regular file")
135 file.splitpath = split_path(f)
136 file.size = fi.Size()
137 batch.files = append(batch.files, file)
138 batch.total_size += file.size
141 // find the rightmost common directory
142 if len(batch.files) == 1 {
143 sp := batch.files[0].splitpath
144 batch.default_name = sp[len(sp)-1]
146 common := batch.files[0].splitpath
147 for _, f := range batch.files {
148 if len(common) > len(f.splitpath) {
149 common = common[:len(f.splitpath)]
152 for i, n := 0, len(common); i < n; i++ {
153 if common[i] != f.splitpath[i] {
159 if len(common) == 0 {
164 if len(common) == 0 {
165 return nil, errors.New("no common rightmost folder was found for a set of queued files")
168 // found the common folder, let's strip that part from splitpath
169 // and setup the default name
170 batch.default_name = common[len(common)-1]
172 lcommon := len(common)
173 for i := range batch.files {
175 f.splitpath = f.splitpath[lcommon:]
178 // and finally sort the files
179 sort.Sort(file_slice(batch.files))
182 // reset the builder state
183 b.batch_state = batch_state{}
189 func (b *Builder) set_defaults() {
190 if b.piece_length == 0 {
191 b.piece_length = 256 * 1024
194 if b.creation_date.IsZero() {
195 b.creation_date = time.Now()
198 if b.created_by == "" {
199 b.created_by = "libtorgo"
202 if b.encoding == "" {
207 func emptyStringsFiltered(ss []string) (ret []string) {
208 for _, s := range ss {
216 func (b *Builder) check_parameters() error {
217 // should be at least one file
218 if len(b.filesmap) == 0 {
219 return errors.New("no files were queued")
222 // let's clean up the announce_list and node_list
223 b.announce_list = cleanUpLists(b.announce_list)
224 b.node_list = emptyStringsFiltered(b.node_list)
226 if len(b.announce_list) == 0 && len(b.node_list) == 0 {
227 return errors.New("no announce group or DHT nodes specified")
230 // Either the node_list or announce_list can be present
232 if len(b.announce_list) > 0 && len(b.node_list) > 0 {
233 return errors.New("announce group and nodes are mutually exclusive")
236 // and clean up the urls
237 b.urls = remove_empty_strings(b.urls)
242 func cleanUpLists(list [][]string) [][]string {
243 newList := make([][]string, 0, len(list))
244 for _, l := range list {
245 l = remove_empty_strings(l)
247 // discard empty announce groups
251 newList = append(newList, l)
256 //----------------------------------------------------------------------------
258 //----------------------------------------------------------------------------
260 // Batch represents a snapshot of a builder state, ready for transforming it
261 // into a torrent file. Note that Batch contains two accessor methods you might
262 // be interested in. The TotalSize is the total size of all the files queued for
263 // hashing, you will use it for status reporting. The DefaultName is an
264 // automatically determined name of the torrent metainfo, you might want to use
265 // it for naming the .torrent file itself.
273 // Get a total size of all the files queued for hashing. Useful in conjunction
274 // with status reports.
275 func (b *Batch) TotalSize() int64 {
279 // Get an automatically determined name of the future torrent metainfo. You can
280 // use it for a .torrent file in case user hasn't provided it specifically.
281 func (b *Batch) DefaultName() string {
282 return b.default_name
285 // Starts a process of building the torrent file. This function does everything
286 // in a separate goroutine and uses up to 'nworkers' of goroutines to perform
287 // SHA1 hashing. Therefore it will return almost immedately. It returns two
288 // channels, the first one is for completion awaiting, the second one is for
289 // getting status reports. Status report is a number of bytes hashed, you can
290 // get the total amount of bytes by inspecting the Batch.TotalSize method return
292 func (b *Batch) Start(w io.Writer, nworkers int) (<-chan error, <-chan int64) {
297 completion := make(chan error)
298 status := make(chan int64)
302 workers := make([]*worker, nworkers)
303 free_workers := make(chan *worker, nworkers)
304 for i := 0; i < nworkers; i++ {
305 workers[i] = new_worker(free_workers)
307 stop_workers := func() {
308 for _, w := range workers {
311 for _, w := range workers {
316 // prepare files for reading
317 fr := files_reader{files: b.files}
318 npieces := (b.total_size + b.piece_length - 1) / b.piece_length
319 b.pieces = make([]byte, 20*npieces)
322 // read all the pieces passing them to workers for hashing
324 for i := int64(0); i < npieces; i++ {
326 data = make([]byte, b.piece_length)
329 nr, err := fr.Read(data)
331 // EOF is not an eror if it was the last piece
345 // cut the data slice to the amount of actual data read
348 data = w.queue(data, b.pieces[20*i:20*i+20])
350 // update and try to send the status report
352 hashed += int64(len(data))
353 data = data[:cap(data)]
356 case status <- hashed:
363 // at this point the hash was calculated and we're ready to
364 // write the torrent file
365 err := b.write_torrent(w)
372 return completion, status
375 func (b *Batch) write_torrent(w io.Writer) error {
378 // Either announce or node lists are allowed - not both
379 if len(b.announce_list) != 0 {
380 td.Announce = b.announce_list[0][0]
381 if len(b.announce_list) != 1 || len(b.announce_list[0]) != 1 {
382 td.AnnounceList = b.announce_list
386 missinggo.CastSlice(&td.Nodes, b.node_list)
387 td.CreationDate = b.creation_date.Unix()
388 td.Comment = b.comment
389 td.CreatedBy = b.created_by
390 td.Encoding = b.encoding
392 case len(b.urls) == 0:
393 case len(b.urls) == 1:
394 td.URLList = b.urls[0]
399 td.Info.PieceLength = b.piece_length
400 td.Info.Pieces = b.pieces
402 td.Info.Name = b.default_name
404 td.Info.Name = b.name
406 if len(b.files) == 1 {
407 td.Info.Length = b.files[0].size
409 td.Info.Files = make([]FileInfo, len(b.files))
410 for i, f := range b.files {
411 td.Info.Files[i] = FileInfo{
417 td.Info.Private = b.private
419 e := bencode.NewEncoder(w)
423 //----------------------------------------------------------------------------
425 //----------------------------------------------------------------------------
427 // splits path into components (dirs and files), works only on absolute paths
428 func split_path(path string) []string {
430 s := make([]string, 0, 5)
434 dir, file = filepath.Split(filepath.Clean(dir))
442 for i, n := 0, len(s)/2; i < n; i++ {
444 s[i], s[i2] = s[i2], s[i]
450 // just a common data between the Builder and the Batch
451 type batch_state struct {
456 announce_list [][]string
458 creation_date time.Time
471 type file_slice []file
473 func (s file_slice) Len() int { return len(s) }
474 func (s file_slice) Less(i, j int) bool { return s[i].abspath < s[j].abspath }
475 func (s file_slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
477 func remove_empty_strings(slice []string) []string {
479 for i, n := 0, len(slice); i < n; i++ {
489 //----------------------------------------------------------------------------
491 //----------------------------------------------------------------------------
502 // returns existing 'data'
503 func (w *worker) queue(data, sha1 []byte) []byte {
511 func (w *worker) stop() {
515 func (w *worker) wait_for_stop() {
519 func new_worker(out chan<- *worker) *worker {
521 msgbox: make(chan bool),
534 copy(w.sha1, sha1[:])
542 //----------------------------------------------------------------------------
544 //----------------------------------------------------------------------------
546 type files_reader struct {
553 func (f *files_reader) Read(data []byte) (int, error) {
554 if f.cur >= len(f.files) {
564 file := &f.files[f.cur]
565 if f.curfile == nil {
567 f.curfile, err = os.Open(file.abspath)
573 // we need to read up to 'len(data)' bytes from current file
574 n := int64(len(data))
576 // unless there is not enough data in this file
577 if file.size-f.off < n {
578 n = file.size - f.off
581 // if there is no data in this file, try next one
583 err := f.curfile.Close()
591 if f.cur >= len(f.files) {
597 // read, handle errors
598 nr, err := f.curfile.Read(data[:n])
605 // ok, we've read nr bytes out of len(data), cut the data slice