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