github.com/anacrolix/torrent@v1.61.0/metainfo/info.go (about)

     1  package metainfo
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"iter"
     8  	"os"
     9  	"path/filepath"
    10  	"slices"
    11  	"sort"
    12  	"strings"
    13  
    14  	"github.com/anacrolix/torrent/segments"
    15  )
    16  
    17  // The info dictionary. See BEP 3 and BEP 52.
    18  type Info struct {
    19  	PieceLength int64 `bencode:"piece length"` // BEP3
    20  	// BEP 3. This can be omitted because isn't needed in non-hybrid v2 infos. See BEP 52.
    21  	Pieces   []byte `bencode:"pieces,omitempty"`
    22  	Name     string `bencode:"name"` // BEP3
    23  	NameUtf8 string `bencode:"name.utf-8,omitempty"`
    24  	Length   int64  `bencode:"length,omitempty"` // BEP3, mutually exclusive with Files
    25  	ExtendedFileAttrs
    26  	Private *bool `bencode:"private,omitempty"` // BEP27
    27  	// TODO: Document this field.
    28  	Source string     `bencode:"source,omitempty"`
    29  	Files  []FileInfo `bencode:"files,omitempty"` // BEP3, mutually exclusive with Length
    30  
    31  	// BEP 52 (BitTorrent v2)
    32  	MetaVersion int64    `bencode:"meta version,omitempty"`
    33  	FileTree    FileTree `bencode:"file tree,omitempty"`
    34  }
    35  
    36  // The Info.Name field is "advisory". For multi-file torrents it's usually a suggested directory
    37  // name. There are situations where we don't want a directory (like using the contents of a torrent
    38  // as the immediate contents of a directory), or the name is invalid. Transmission will inject the
    39  // name of the torrent file if it doesn't like the name, resulting in a different infohash
    40  // (https://github.com/transmission/transmission/issues/1775). To work around these situations, we
    41  // will use a sentinel name for compatibility with Transmission and to signal to our own client that
    42  // we intended to have no directory name. By exposing it in the API we can check for references to
    43  // this behaviour within this implementation.
    44  const NoName = "-"
    45  
    46  // This is a helper that sets Files and Pieces from a root path and its children.
    47  func (info *Info) BuildFromFilePath(root string) (err error) {
    48  	info.Name = func() string {
    49  		b := filepath.Base(root)
    50  		switch b {
    51  		case ".", "..", string(filepath.Separator):
    52  			return NoName
    53  		default:
    54  			return b
    55  		}
    56  	}()
    57  	info.Files = nil
    58  	err = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
    59  		if err != nil {
    60  			return err
    61  		}
    62  		if fi.IsDir() {
    63  			// Directories are implicit in torrent files.
    64  			return nil
    65  		} else if path == root {
    66  			// The root is a file.
    67  			info.Length = fi.Size()
    68  			return nil
    69  		}
    70  		relPath, err := filepath.Rel(root, path)
    71  		if err != nil {
    72  			return fmt.Errorf("error getting relative path: %s", err)
    73  		}
    74  		info.Files = append(info.Files, FileInfo{
    75  			Path:   strings.Split(relPath, string(filepath.Separator)),
    76  			Length: fi.Size(),
    77  		})
    78  		return nil
    79  	})
    80  	if err != nil {
    81  		return
    82  	}
    83  	sort.Slice(info.Files, func(i, j int) bool {
    84  		l, r := info.Files[i], info.Files[j]
    85  		return strings.Join(l.BestPath(), "/") < strings.Join(r.BestPath(), "/")
    86  	})
    87  	if info.PieceLength == 0 {
    88  		info.PieceLength = ChoosePieceLength(info.TotalLength())
    89  	}
    90  	err = info.GeneratePieces(func(fi FileInfo) (io.ReadCloser, error) {
    91  		return os.Open(filepath.Join(root, strings.Join(fi.BestPath(), string(filepath.Separator))))
    92  	})
    93  	if err != nil {
    94  		err = fmt.Errorf("error generating pieces: %s", err)
    95  	}
    96  	return
    97  }
    98  
    99  // Concatenates all the files in the torrent into w. open is a function that
   100  // gets at the contents of the given file.
   101  func (info *Info) writeFiles(w io.Writer, open func(fi FileInfo) (io.ReadCloser, error)) error {
   102  	for _, fi := range info.UpvertedFiles() {
   103  		r, err := open(fi)
   104  		if err != nil {
   105  			return fmt.Errorf("error opening %v: %s", fi, err)
   106  		}
   107  		wn, err := io.CopyN(w, r, fi.Length)
   108  		r.Close()
   109  		if wn != fi.Length {
   110  			return fmt.Errorf("error copying %v: %s", fi, err)
   111  		}
   112  	}
   113  	return nil
   114  }
   115  
   116  // Sets Pieces (the block of piece hashes in the Info) by using the passed
   117  // function to get at the torrent data.
   118  func (info *Info) GeneratePieces(open func(fi FileInfo) (io.ReadCloser, error)) (err error) {
   119  	if info.PieceLength == 0 {
   120  		return errors.New("piece length must be non-zero")
   121  	}
   122  	pr, pw := io.Pipe()
   123  	go func() {
   124  		err := info.writeFiles(pw, open)
   125  		pw.CloseWithError(err)
   126  	}()
   127  	defer pr.Close()
   128  	info.Pieces, err = GeneratePieces(pr, info.PieceLength, nil)
   129  	return
   130  }
   131  
   132  func (info *Info) TotalLength() (ret int64) {
   133  	for _, fi := range info.UpvertedFiles() {
   134  		ret += fi.Length
   135  	}
   136  	return
   137  }
   138  
   139  func (info *Info) NumPieces() (num int) {
   140  	if info.HasV2() {
   141  		info.FileTree.Walk(nil, func(path []string, ft *FileTree) {
   142  			num += int((ft.File.Length + info.PieceLength - 1) / info.PieceLength)
   143  		})
   144  		return
   145  	}
   146  	return len(info.Pieces) / 20
   147  }
   148  
   149  // Whether all files share the same top-level directory name. If they don't, Info.Name is usually used.
   150  func (info *Info) IsDir() bool {
   151  	if info.HasV2() {
   152  		return info.FileTree.IsDir()
   153  	}
   154  	// I wonder if we should check for the existence of Info.Length here instead.
   155  	return len(info.Files) != 0
   156  }
   157  
   158  // The files field, converted up from the old single-file in the parent info dict if necessary. This
   159  // is a helper to avoid having to conditionally handle single and multi-file torrent infos.
   160  func (info *Info) UpvertedFiles() (files []FileInfo) {
   161  	return slices.Collect(info.UpvertedFilesIter())
   162  }
   163  
   164  // The files field, converted up from the old single-file in the parent info dict if necessary. This
   165  // is a helper to avoid having to conditionally handle single and multi-file torrent infos.
   166  func (info *Info) UpvertedFilesIter() iter.Seq[FileInfo] {
   167  	if info.HasV2() {
   168  		return info.FileTree.upvertedFiles(info.PieceLength)
   169  	}
   170  	return info.UpvertedV1Files()
   171  }
   172  
   173  // UpvertedFiles but specific to the files listed in the v1 info fields. This will include padding
   174  // files for example that wouldn't appear in v2 file trees.
   175  func (info *Info) UpvertedV1Files() iter.Seq[FileInfo] {
   176  	return func(yield func(FileInfo) bool) {
   177  		if len(info.Files) == 0 {
   178  			yield(FileInfo{
   179  				Length: info.Length,
   180  				// Callers should determine that Info.Name is the basename, and
   181  				// thus a regular file.
   182  				Path: nil,
   183  			})
   184  		}
   185  		var offset int64
   186  		for _, fi := range info.Files {
   187  			fi.TorrentOffset = offset
   188  			offset += fi.Length
   189  			if !yield(fi) {
   190  				return
   191  			}
   192  		}
   193  	}
   194  }
   195  
   196  func (info *Info) Piece(index int) Piece {
   197  	return Piece{info, index}
   198  }
   199  
   200  func (info *Info) BestName() string {
   201  	if info.NameUtf8 != "" {
   202  		return info.NameUtf8
   203  	}
   204  	return info.Name
   205  }
   206  
   207  // Whether the Info can be used as a v2 info dict, including having a V2 infohash.
   208  func (info *Info) HasV2() bool {
   209  	return info.MetaVersion == 2
   210  }
   211  
   212  func (info *Info) HasV1() bool {
   213  	// See Upgrade Path in BEP 52.
   214  	return info.MetaVersion == 0 || info.MetaVersion == 1 || info.Files != nil || info.Length != 0 || len(info.Pieces) != 0
   215  }
   216  
   217  func (info *Info) FilesArePieceAligned() bool {
   218  	return info.HasV2()
   219  }
   220  
   221  func (info *Info) FileSegmentsIndex() segments.Index {
   222  	return segments.NewIndexFromSegments(slices.Collect(func(yield func(segments.Extent) bool) {
   223  		for fi := range info.UpvertedFilesIter() {
   224  			yield(segments.Extent{fi.TorrentOffset, fi.Length})
   225  		}
   226  	}))
   227  }
   228  
   229  // TODO: Add NumFiles helper?