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