defer cl.unlock()
t.initialPieceCheckDisabled = spec.DisableInitialPieceCheck
for _, url := range spec.Webseeds {
- t.addWebSeed(url)
+ t.addWebSeed(url, spec.EncodeWebSeedUrl)
}
for _, peerAddr := range spec.PeerAddrs {
t.addPeer(PeerInfo{
// Whether to allow data download or upload
DisallowDataUpload bool
DisallowDataDownload bool
+
+ // Custom encoder for webseed URLs
+ // Leave nil to use the default (url.QueryEscape)
+ EncodeWebSeedUrl func(string) string
}
func TorrentSpecFromMagnetUri(uri string) (spec *TorrentSpec, err error) {
return &t.cl.config.Callbacks
}
-func (t *Torrent) AddWebSeeds(urls []string) {
+type AddWebSeedsOptions struct {
+ // Custom encoder for webseed URLs
+ // Leave nil to use the default (url.QueryEscape)
+ EncodeUrl func(string) string
+}
+
+
+// Add web seeds to the torrent.
+// If AddWebSeedsOptions is not nil, only the first one is used.
+func (t *Torrent) AddWebSeeds(urls []string, opts ...AddWebSeedsOptions) {
t.cl.lock()
defer t.cl.unlock()
for _, u := range urls {
- t.addWebSeed(u)
+ if opts == nil {
+ t.addWebSeed(u, nil)
+ } else {
+ t.addWebSeed(u, opts[0].EncodeUrl)
+ }
}
}
-func (t *Torrent) addWebSeed(url string) {
+func (t *Torrent) addWebSeed(url string, encodeUrl func(string) string) {
if t.cl.config.DisableWebseeds {
return
}
r: r,
}
},
+ EncodeUrl: encodeUrl,
},
activeRequests: make(map[Request]webseed.Request, maxRequests),
maxRequests: maxRequests,
r.cancel()
}
+type Spec struct {
+ Urls []string
+ EncodeUrl func(string) string
+}
+
type Client struct {
HttpClient *http.Client
Url string
// private in the future, if Client ever starts removing pieces.
Pieces roaring.Bitmap
ResponseBodyWrapper ResponseBodyWrapper
+ EncodeUrl func(string) string
}
type ResponseBodyWrapper func(io.Reader) io.Reader
ctx, cancel := context.WithCancel(context.Background())
var requestParts []requestPart
if !ws.fileIndex.Locate(r, func(i int, e segments.Extent) bool {
- req, err := NewRequest(ws.Url, i, ws.info, e.Start, e.Length)
+ req, err := NewRequestWithCustomUrlEncoding(
+ ws.Url, i, ws.info, e.Start, e.Length,
+ ws.EncodeUrl,
+ )
if err != nil {
panic(err)
}
// Escapes path name components suitable for appending to a webseed URL. This works for converting
// S3 object keys to URLs too.
+// Contrary to the name, this actually does a QueryEscape, rather than a
+// PathEscape. This works better with most S3 providers. You can use
+// EscapePathWithCustomEncoding for a custom encoding.
func EscapePath(pathComps []string) string {
+ return escapePath(pathComps, nil)
+}
+
+func escapePath(pathComps []string, encodeUrl func(string) string) string {
+ if encodeUrl == nil {
+ encodeUrl = url.QueryEscape
+ }
return path.Join(
func() (ret []string) {
for _, comp := range pathComps {
- ret = append(ret, url.QueryEscape(comp))
+ ret = append(ret, encodeUrl(comp))
}
return
}()...,
)
}
-func trailingPath(infoName string, fileComps []string) string {
- return EscapePath(append([]string{infoName}, fileComps...))
+func trailingPath(infoName string, fileComps []string, encodeUrl func(string) string) string {
+ return escapePath(append([]string{infoName}, fileComps...), encodeUrl)
}
// Creates a request per BEP 19.
func NewRequest(url_ string, fileIndex int, info *metainfo.Info, offset, length int64) (*http.Request, error) {
+ return newRequest(url_, fileIndex, info, offset, length, nil)
+}
+
+func NewRequestWithCustomUrlEncoding(
+ url_ string, fileIndex int,
+ info *metainfo.Info,
+ offset, length int64,
+ encodeUrl func(string) string,
+) (*http.Request, error) {
+ return newRequest(url_, fileIndex, info, offset, length, encodeUrl)
+}
+
+func newRequest(
+ url_ string, fileIndex int,
+ info *metainfo.Info,
+ offset, length int64,
+ encodeUrl func(string) string,
+) (*http.Request, error) {
fileInfo := info.UpvertedFiles()[fileIndex]
if strings.HasSuffix(url_, "/") {
// BEP specifies that we append the file path. We need to escape each component of the path
// for things like spaces and '#'.
- url_ += trailingPath(info.Name, fileInfo.Path)
+ url_ += trailingPath(info.Name, fileInfo.Path, encodeUrl)
}
req, err := http.NewRequest(http.MethodGet, url_, nil)
if err != nil {
func TestTrailingPath(t *testing.T) {
c := qt.New(t)
test := func(parts []string, result string) {
- unescaped, err := url.QueryUnescape(trailingPath(parts[0], parts[1:]))
+ unescaped, err := url.QueryUnescape(trailingPath(parts[0], parts[1:], url.QueryEscape))
if !c.Check(err, qt.IsNil) {
return
}
}
func TestTrailingPathForEmptyInfoName(t *testing.T) {
- qt.Check(t, trailingPath("", []string{`ノ┬─┬ノ ︵ ( \o°o)\`}), qt.Equals, "%E3%83%8E%E2%94%AC%E2%94%80%E2%94%AC%E3%83%8E+%EF%B8%B5+%28+%5Co%C2%B0o%29%5C")
- qt.Check(t, trailingPath("", []string{"hello", "world"}), qt.Equals, "hello/world")
- qt.Check(t, trailingPath("war", []string{"and", "peace"}), qt.Equals, "war/and/peace")
+ qt.Check(t, trailingPath("", []string{`ノ┬─┬ノ ︵ ( \o°o)\`}, url.QueryEscape), qt.Equals, "%E3%83%8E%E2%94%AC%E2%94%80%E2%94%AC%E3%83%8E+%EF%B8%B5+%28+%5Co%C2%B0o%29%5C")
+ qt.Check(t, trailingPath("", []string{"hello", "world"}, url.QueryEscape), qt.Equals, "hello/world")
+ qt.Check(t, trailingPath("war", []string{"and", "peace"}, url.QueryEscape), qt.Equals, "war/and/peace")
}