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?