]> Sergey Matveev's repositories - btrtrc.git/blob - metainfo/metainfo.go
metainfo: Move method in wrong file
[btrtrc.git] / metainfo / metainfo.go
1 package metainfo
2
3 import (
4         "crypto/sha1"
5         "errors"
6         "fmt"
7         "io"
8         "log"
9         "os"
10         "path/filepath"
11         "strings"
12         "time"
13
14         "github.com/anacrolix/missinggo"
15
16         "github.com/anacrolix/torrent/bencode"
17 )
18
19 // Information specific to a single file inside the MetaInfo structure.
20 type FileInfo struct {
21         Length int64    `bencode:"length"`
22         Path   []string `bencode:"path"`
23 }
24
25 // Load a MetaInfo from an io.Reader. Returns a non-nil error in case of
26 // failure.
27 func Load(r io.Reader) (*MetaInfo, error) {
28         var mi MetaInfo
29         d := bencode.NewDecoder(r)
30         err := d.Decode(&mi)
31         if err != nil {
32                 return nil, err
33         }
34         return &mi, nil
35 }
36
37 // Convenience function for loading a MetaInfo from a file.
38 func LoadFromFile(filename string) (*MetaInfo, error) {
39         f, err := os.Open(filename)
40         if err != nil {
41                 return nil, err
42         }
43         defer f.Close()
44         return Load(f)
45 }
46
47 // The info dictionary.
48 type Info struct {
49         PieceLength int64      `bencode:"piece length"`
50         Pieces      []byte     `bencode:"pieces"`
51         Name        string     `bencode:"name"`
52         Length      int64      `bencode:"length,omitempty"`
53         Private     *bool      `bencode:"private,omitempty"`
54         Files       []FileInfo `bencode:"files,omitempty"`
55 }
56
57 // This is a helper that sets Files and Pieces from a root path and its
58 // children.
59 func (info *Info) BuildFromFilePath(root string) (err error) {
60         info.Name = filepath.Base(root)
61         info.Files = nil
62         err = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
63                 if err != nil {
64                         return err
65                 }
66                 if fi.IsDir() {
67                         // Directories are implicit in torrent files.
68                         return nil
69                 } else if path == root {
70                         // The root is a file.
71                         info.Length = fi.Size()
72                         return nil
73                 }
74                 relPath, err := filepath.Rel(root, path)
75                 log.Println(relPath, err)
76                 if err != nil {
77                         return fmt.Errorf("error getting relative path: %s", err)
78                 }
79                 info.Files = append(info.Files, FileInfo{
80                         Path:   strings.Split(relPath, string(filepath.Separator)),
81                         Length: fi.Size(),
82                 })
83                 return nil
84         })
85         if err != nil {
86                 return
87         }
88         missinggo.SortSlice(info.Files, func(l, r FileInfo) bool {
89                 return strings.Join(l.Path, "/") < strings.Join(r.Path, "/")
90         })
91         err = info.GeneratePieces(func(fi FileInfo) (io.ReadCloser, error) {
92                 return os.Open(filepath.Join(root, strings.Join(fi.Path, string(filepath.Separator))))
93         })
94         if err != nil {
95                 err = fmt.Errorf("error generating pieces: %s", err)
96         }
97         return
98 }
99
100 func (info *Info) writeFiles(w io.Writer, open func(fi FileInfo) (io.ReadCloser, error)) error {
101         for _, fi := range info.UpvertedFiles() {
102                 r, err := open(fi)
103                 if err != nil {
104                         return fmt.Errorf("error opening %v: %s", fi, err)
105                 }
106                 wn, err := io.CopyN(w, r, fi.Length)
107                 r.Close()
108                 if wn != fi.Length || err != nil {
109                         return fmt.Errorf("error hashing %v: %s", fi, err)
110                 }
111         }
112         return nil
113 }
114
115 // Set info.Pieces by hashing info.Files.
116 func (info *Info) GeneratePieces(open func(fi FileInfo) (io.ReadCloser, error)) error {
117         if info.PieceLength == 0 {
118                 return errors.New("piece length must be non-zero")
119         }
120         pr, pw := io.Pipe()
121         go func() {
122                 err := info.writeFiles(pw, open)
123                 pw.CloseWithError(err)
124         }()
125         defer pr.Close()
126         var pieces []byte
127         for {
128                 hasher := sha1.New()
129                 wn, err := io.CopyN(hasher, pr, info.PieceLength)
130                 if err == io.EOF {
131                         err = nil
132                 }
133                 if err != nil {
134                         return err
135                 }
136                 if wn == 0 {
137                         break
138                 }
139                 pieces = hasher.Sum(pieces)
140                 if wn < info.PieceLength {
141                         break
142                 }
143         }
144         info.Pieces = pieces
145         return nil
146 }
147
148 func (info *Info) TotalLength() (ret int64) {
149         if info.IsDir() {
150                 for _, fi := range info.Files {
151                         ret += fi.Length
152                 }
153         } else {
154                 ret = info.Length
155         }
156         return
157 }
158
159 func (info *Info) NumPieces() int {
160         if len(info.Pieces)%20 != 0 {
161                 panic(len(info.Pieces))
162         }
163         return len(info.Pieces) / 20
164 }
165
166 func (info *Info) IsDir() bool {
167         return len(info.Files) != 0
168 }
169
170 // The files field, converted up from the old single-file in the parent info
171 // dict if necessary. This is a helper to avoid having to conditionally handle
172 // single and multi-file torrent infos.
173 func (info *Info) UpvertedFiles() []FileInfo {
174         if len(info.Files) == 0 {
175                 return []FileInfo{{
176                         Length: info.Length,
177                         // Callers should determine that Info.Name is the basename, and
178                         // thus a regular file.
179                         Path: nil,
180                 }}
181         }
182         return info.Files
183 }
184
185 type MetaInfo struct {
186         Info         InfoEx      `bencode:"info"`
187         Announce     string      `bencode:"announce,omitempty"`
188         AnnounceList [][]string  `bencode:"announce-list,omitempty"`
189         Nodes        []Node      `bencode:"nodes,omitempty"`
190         CreationDate int64       `bencode:"creation date,omitempty"`
191         Comment      string      `bencode:"comment,omitempty"`
192         CreatedBy    string      `bencode:"created by,omitempty"`
193         Encoding     string      `bencode:"encoding,omitempty"`
194         URLList      interface{} `bencode:"url-list,omitempty"`
195 }
196
197 // Encode to bencoded form.
198 func (mi *MetaInfo) Write(w io.Writer) error {
199         return bencode.NewEncoder(w).Encode(mi)
200 }
201
202 // Set good default values in preparation for creating a new MetaInfo file.
203 func (mi *MetaInfo) SetDefaults() {
204         mi.Comment = "yoloham"
205         mi.CreatedBy = "github.com/anacrolix/torrent"
206         mi.CreationDate = time.Now().Unix()
207         mi.Info.PieceLength = 256 * 1024
208 }
209
210 // Creates a Magnet from a MetaInfo.
211 func (mi *MetaInfo) Magnet() (m Magnet) {
212         for _, tier := range mi.AnnounceList {
213                 for _, tracker := range tier {
214                         m.Trackers = append(m.Trackers, tracker)
215                 }
216         }
217         m.DisplayName = mi.Info.Name
218         m.InfoHash = mi.Info.Hash()
219         return
220 }