13 "github.com/anacrolix/torrent/bencode"
16 //----------------------------------------------------------------------------
18 //----------------------------------------------------------------------------
20 // The Builder type is responsible for .torrent files construction. Just
21 // instantiate it, call necessary methods and then call the .Build method. While
22 // waiting for completion you can use 'status' channel to get status reports.
25 filesmap map[string]bool
28 // Adds a file to the builder queue. You may add one or more files.
29 func (b *Builder) AddFile(filename string) {
30 if b.filesmap == nil {
31 b.filesmap = make(map[string]bool)
34 filename, err := filepath.Abs(filename)
38 b.filesmap[filename] = true
41 // Defines a name of the future torrent file. For single file torrents it's the
42 // recommended name of the contained file. For multiple files torrents it's the
43 // recommended name of the directory in which all of them will be
44 // stored. Calling this function is not required. In case if no name was
45 // specified, the builder will try to automatically assign it. It will use the
46 // name of the file if there is only one file in the queue or it will try to
47 // find the rightmost common directory of all the queued files and use its name as
48 // a torrent name. In case if name cannot be assigned automatically, it will use
49 // "unknown" as a torrent name.
50 func (b *Builder) SetName(name string) {
54 // Sets the length of a piece in the torrent file in bytes. The default is
56 func (b *Builder) SetPieceLength(length int64) {
57 b.piece_length = length
60 // Sets the "private" flag. The default is false.
61 func (b *Builder) SetPrivate(v bool) {
65 // Add announce URL group. TODO: better explanation.
66 func (b *Builder) AddAnnounceGroup(group []string) {
67 b.announce_list = append(b.announce_list, group)
70 // Sets creation date. The default is time.Now() when the .Build method was
72 func (b *Builder) SetCreationDate(date time.Time) {
73 b.creation_date = date
76 // Sets the comment. The default is no comment.
77 func (b *Builder) SetComment(comment string) {
81 // Sets the "created by" parameter. The default is "libtorgo".
82 func (b *Builder) SetCreatedBy(createdby string) {
83 b.created_by = createdby
86 // Sets the "encoding" parameter. The default is "UTF-8".
87 func (b *Builder) SetEncoding(encoding string) {
91 // Add WebSeed URL to the list.
92 func (b *Builder) AddWebSeedURL(url string) {
93 b.urls = append(b.urls, url)
96 // Finalizes the Builder state and makes a Batch out of it. After calling that
97 // method, Builder becomes empty and you can use it to create another Batch if
99 func (b *Builder) Submit() (*Batch, error) {
100 err := b.check_parameters()
107 batch_state: b.batch_state,
110 const non_regular = os.ModeDir | os.ModeSymlink |
111 os.ModeDevice | os.ModeNamedPipe | os.ModeSocket
113 // convert a map to a slice, calculate sizes and split paths
115 batch.files = make([]file, 0, 10)
116 for f, _ := range b.filesmap {
118 fi, err := os.Stat(f)
123 if fi.Mode()&non_regular != 0 {
124 return nil, errors.New(f + " is not a regular file")
128 file.splitpath = split_path(f)
129 file.size = fi.Size()
130 batch.files = append(batch.files, file)
131 batch.total_size += file.size
134 // find the rightmost common directory
135 if len(batch.files) == 1 {
136 sp := batch.files[0].splitpath
137 batch.default_name = sp[len(sp)-1]
139 common := batch.files[0].splitpath
140 for _, f := range batch.files {
141 if len(common) > len(f.splitpath) {
142 common = common[:len(f.splitpath)]
145 for i, n := 0, len(common); i < n; i++ {
146 if common[i] != f.splitpath[i] {
152 if len(common) == 0 {
157 if len(common) == 0 {
158 return nil, errors.New("no common rightmost folder was found for a set of queued files")
161 // found the common folder, let's strip that part from splitpath
162 // and setup the default name
163 batch.default_name = common[len(common)-1]
165 lcommon := len(common)
166 for i := range batch.files {
168 f.splitpath = f.splitpath[lcommon:]
171 // and finally sort the files
172 sort.Sort(file_slice(batch.files))
175 // reset the builder state
176 b.batch_state = batch_state{}
182 func (b *Builder) set_defaults() {
183 if b.piece_length == 0 {
184 b.piece_length = 256 * 1024
187 if b.creation_date.IsZero() {
188 b.creation_date = time.Now()
191 if b.created_by == "" {
192 b.created_by = "libtorgo"
195 if b.encoding == "" {
200 func (b *Builder) check_parameters() error {
201 // should be at least one file
202 if len(b.filesmap) == 0 {
203 return errors.New("no files were queued")
206 // let's clean up the announce_list
207 newal := make([][]string, 0, len(b.announce_list))
208 for _, ag := range b.announce_list {
209 ag = remove_empty_strings(ag)
211 // discard empty announce groups
215 newal = append(newal, ag)
217 b.announce_list = newal
218 if len(b.announce_list) == 0 {
219 return errors.New("no announce groups were specified")
222 // and clean up the urls
223 b.urls = remove_empty_strings(b.urls)
228 //----------------------------------------------------------------------------
230 //----------------------------------------------------------------------------
232 // Batch represents a snapshot of a builder state, ready for transforming it
233 // into a torrent file. Note that Batch contains two accessor methods you might
234 // be interested in. The TotalSize is the total size of all the files queued for
235 // hashing, you will use it for status reporting. The DefaultName is an
236 // automatically determined name of the torrent metainfo, you might want to use
237 // it for naming the .torrent file itself.
245 // Get a total size of all the files queued for hashing. Useful in conjunction
246 // with status reports.
247 func (b *Batch) TotalSize() int64 {
251 // Get an automatically determined name of the future torrent metainfo. You can
252 // use it for a .torrent file in case user hasn't provided it specifically.
253 func (b *Batch) DefaultName() string {
254 return b.default_name
257 // Starts a process of building the torrent file. This function does everything
258 // in a separate goroutine and uses up to 'nworkers' of goroutines to perform
259 // SHA1 hashing. Therefore it will return almost immedately. It returns two
260 // channels, the first one is for completion awaiting, the second one is for
261 // getting status reports. Status report is a number of bytes hashed, you can
262 // get the total amount of bytes by inspecting the Batch.TotalSize method return
264 func (b *Batch) Start(w io.Writer, nworkers int) (<-chan error, <-chan int64) {
269 completion := make(chan error)
270 status := make(chan int64)
274 workers := make([]*worker, nworkers)
275 free_workers := make(chan *worker, nworkers)
276 for i := 0; i < nworkers; i++ {
277 workers[i] = new_worker(free_workers)
279 stop_workers := func() {
280 for _, w := range workers {
283 for _, w := range workers {
288 // prepare files for reading
289 fr := files_reader{files: b.files}
290 npieces := b.total_size/b.piece_length + 1
291 b.pieces = make([]byte, 20*npieces)
294 // read all the pieces passing them to workers for hashing
296 for i := int64(0); i < npieces; i++ {
298 data = make([]byte, b.piece_length)
301 nr, err := fr.Read(data)
303 // EOF is not an eror if it was the last piece
317 // cut the data slice to the amount of actual data read
320 data = w.queue(data, b.pieces[20*i:20*i+20])
322 // update and try to send the status report
324 hashed += int64(len(data))
325 data = data[:cap(data)]
328 case status <- hashed:
335 // at this point the hash was calculated and we're ready to
336 // write the torrent file
337 err := b.write_torrent(w)
344 return completion, status
347 func (b *Batch) write_torrent(w io.Writer) error {
349 td.Announce = b.announce_list[0][0]
350 if len(b.announce_list) != 1 || len(b.announce_list[0]) != 1 {
351 td.AnnounceList = b.announce_list
353 td.CreationDate = b.creation_date.Unix()
354 td.Comment = b.comment
355 td.CreatedBy = b.created_by
356 td.Encoding = b.encoding
358 case len(b.urls) == 0:
359 case len(b.urls) == 1:
360 td.URLList = b.urls[0]
365 td.Info.PieceLength = b.piece_length
366 td.Info.Pieces = b.pieces
368 td.Info.Name = b.default_name
370 td.Info.Name = b.name
372 if len(b.files) == 1 {
373 td.Info.Length = b.files[0].size
375 td.Info.Files = make([]FileInfo, len(b.files))
376 for i, f := range b.files {
377 td.Info.Files[i] = FileInfo{
383 td.Info.Private = b.private
385 e := bencode.NewEncoder(w)
389 //----------------------------------------------------------------------------
391 //----------------------------------------------------------------------------
393 // splits path into components (dirs and files), works only on absolute paths
394 func split_path(path string) []string {
396 s := make([]string, 0, 5)
400 dir, file = filepath.Split(filepath.Clean(dir))
408 for i, n := 0, len(s)/2; i < n; i++ {
410 s[i], s[i2] = s[i2], s[i]
416 // just a common data between the Builder and the Batch
417 type batch_state struct {
422 announce_list [][]string
423 creation_date time.Time
436 type file_slice []file
438 func (s file_slice) Len() int { return len(s) }
439 func (s file_slice) Less(i, j int) bool { return s[i].abspath < s[j].abspath }
440 func (s file_slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
442 func remove_empty_strings(slice []string) []string {
444 for i, n := 0, len(slice); i < n; i++ {
454 //----------------------------------------------------------------------------
456 //----------------------------------------------------------------------------
467 // returns existing 'data'
468 func (w *worker) queue(data, sha1 []byte) []byte {
476 func (w *worker) stop() {
480 func (w *worker) wait_for_stop() {
484 func new_worker(out chan<- *worker) *worker {
486 msgbox: make(chan bool),
499 copy(w.sha1, sha1[:])
507 //----------------------------------------------------------------------------
509 //----------------------------------------------------------------------------
511 type files_reader struct {
518 func (f *files_reader) Read(data []byte) (int, error) {
519 if f.cur >= len(f.files) {
529 file := &f.files[f.cur]
530 if f.curfile == nil {
532 f.curfile, err = os.Open(file.abspath)
538 // we need to read up to 'len(data)' bytes from current file
539 n := int64(len(data))
541 // unless there is not enough data in this file
542 if file.size-f.off < n {
543 n = file.size - f.off
546 // if there is no data in this file, try next one
548 err := f.curfile.Close()
556 if f.cur >= len(f.files) {
562 // read, handle errors
563 nr, err := f.curfile.Read(data[:n])
570 // ok, we've read nr bytes out of len(data), cut the data slice