info.go 6.2 KB

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