]> Sergey Matveev's repositories - btrtrc.git/blob - metainfo/info.go
metainfo: Add Magnet.Params for more open handling
[btrtrc.git] / metainfo / info.go
1 package metainfo
2
3 import (
4         "crypto/sha1"
5         "errors"
6         "fmt"
7         "io"
8         "os"
9         "path/filepath"
10         "strings"
11
12         "github.com/anacrolix/missinggo/slices"
13 )
14
15 // The info dictionary.
16 type Info struct {
17         PieceLength int64  `bencode:"piece length"`
18         Pieces      []byte `bencode:"pieces"`
19         Name        string `bencode:"name"`
20         Length      int64  `bencode:"length,omitempty"`
21         Private     *bool  `bencode:"private,omitempty"`
22         // TODO: Document this field.
23         Source string     `bencode:"source,omitempty"`
24         Files  []FileInfo `bencode:"files,omitempty"`
25 }
26
27 // This is a helper that sets Files and Pieces from a root path and its
28 // children.
29 func (info *Info) BuildFromFilePath(root string) (err error) {
30         info.Name = filepath.Base(root)
31         info.Files = nil
32         err = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
33                 if err != nil {
34                         return err
35                 }
36                 if fi.IsDir() {
37                         // Directories are implicit in torrent files.
38                         return nil
39                 } else if path == root {
40                         // The root is a file.
41                         info.Length = fi.Size()
42                         return nil
43                 }
44                 relPath, err := filepath.Rel(root, path)
45                 if err != nil {
46                         return fmt.Errorf("error getting relative path: %s", err)
47                 }
48                 info.Files = append(info.Files, FileInfo{
49                         Path:   strings.Split(relPath, string(filepath.Separator)),
50                         Length: fi.Size(),
51                 })
52                 return nil
53         })
54         if err != nil {
55                 return
56         }
57         slices.Sort(info.Files, func(l, r FileInfo) bool {
58                 return strings.Join(l.Path, "/") < strings.Join(r.Path, "/")
59         })
60         err = info.GeneratePieces(func(fi FileInfo) (io.ReadCloser, error) {
61                 return os.Open(filepath.Join(root, strings.Join(fi.Path, string(filepath.Separator))))
62         })
63         if err != nil {
64                 err = fmt.Errorf("error generating pieces: %s", err)
65         }
66         return
67 }
68
69 // Concatenates all the files in the torrent into w. open is a function that
70 // gets at the contents of the given file.
71 func (info *Info) writeFiles(w io.Writer, open func(fi FileInfo) (io.ReadCloser, error)) error {
72         for _, fi := range info.UpvertedFiles() {
73                 r, err := open(fi)
74                 if err != nil {
75                         return fmt.Errorf("error opening %v: %s", fi, err)
76                 }
77                 wn, err := io.CopyN(w, r, fi.Length)
78                 r.Close()
79                 if wn != fi.Length {
80                         return fmt.Errorf("error copying %v: %s", fi, err)
81                 }
82         }
83         return nil
84 }
85
86 // Sets Pieces (the block of piece hashes in the Info) by using the passed
87 // function to get at the torrent data.
88 func (info *Info) GeneratePieces(open func(fi FileInfo) (io.ReadCloser, error)) error {
89         if info.PieceLength == 0 {
90                 return errors.New("piece length must be non-zero")
91         }
92         pr, pw := io.Pipe()
93         go func() {
94                 err := info.writeFiles(pw, open)
95                 pw.CloseWithError(err)
96         }()
97         defer pr.Close()
98         var pieces []byte
99         for {
100                 hasher := sha1.New()
101                 wn, err := io.CopyN(hasher, pr, info.PieceLength)
102                 if err == io.EOF {
103                         err = nil
104                 }
105                 if err != nil {
106                         return err
107                 }
108                 if wn == 0 {
109                         break
110                 }
111                 pieces = hasher.Sum(pieces)
112                 if wn < info.PieceLength {
113                         break
114                 }
115         }
116         info.Pieces = pieces
117         return nil
118 }
119
120 func (info *Info) TotalLength() (ret int64) {
121         if info.IsDir() {
122                 for _, fi := range info.Files {
123                         ret += fi.Length
124                 }
125         } else {
126                 ret = info.Length
127         }
128         return
129 }
130
131 func (info *Info) NumPieces() int {
132         return len(info.Pieces) / 20
133 }
134
135 func (info *Info) IsDir() bool {
136         return len(info.Files) != 0
137 }
138
139 // The files field, converted up from the old single-file in the parent info
140 // dict if necessary. This is a helper to avoid having to conditionally handle
141 // single and multi-file torrent infos.
142 func (info *Info) UpvertedFiles() []FileInfo {
143         if len(info.Files) == 0 {
144                 return []FileInfo{{
145                         Length: info.Length,
146                         // Callers should determine that Info.Name is the basename, and
147                         // thus a regular file.
148                         Path: nil,
149                 }}
150         }
151         return info.Files
152 }
153
154 func (info *Info) Piece(index int) Piece {
155         return Piece{info, pieceIndex(index)}
156 }