139 lines
4.4 KiB
Go
139 lines
4.4 KiB
Go
|
|
package engine
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/anacrolix/torrent"
|
||
|
|
"github.com/anacrolix/torrent/bencode"
|
||
|
|
"github.com/anacrolix/torrent/metainfo"
|
||
|
|
)
|
||
|
|
|
||
|
|
// SeedFile builds a single-file torrent from an arbitrary on-disk file
|
||
|
|
// and adds it to an existing torrent client so the WebRTC peer wire
|
||
|
|
// (already configured on the client) can serve the file to a browser
|
||
|
|
// that knows the resulting info-hash.
|
||
|
|
//
|
||
|
|
// Returns the generated info-hash. The torrent is left attached to the
|
||
|
|
// client — caller is responsible for keeping it alive while a browser
|
||
|
|
// is watching. Drop it via Client.RemoveTorrent / Torrent.Drop when
|
||
|
|
// idle to free resources.
|
||
|
|
//
|
||
|
|
// Behaviour notes:
|
||
|
|
// - The file must already exist; no download is attempted.
|
||
|
|
// - Piece length follows the libtorrent ladder (16 KiB → 4 MiB).
|
||
|
|
// - The torrent is "complete" from the agent's POV — it has every
|
||
|
|
// piece — so the upload-only flow kicks in immediately.
|
||
|
|
// - WebRTC peer behaviour comes from the client config the caller
|
||
|
|
// constructed; SeedFile does not toggle DisableWebtorrent itself.
|
||
|
|
// If the operator's [downloads.webrtc].enabled = false, the file
|
||
|
|
// is still added but no browser will discover it via WSS tracker.
|
||
|
|
func SeedFile(client *torrent.Client, filePath string, trackerURLs []string) (metainfo.Hash, error) {
|
||
|
|
if client == nil {
|
||
|
|
return metainfo.Hash{}, errors.New("seed_file: torrent client is nil")
|
||
|
|
}
|
||
|
|
if filePath == "" {
|
||
|
|
return metainfo.Hash{}, errors.New("seed_file: filePath is empty")
|
||
|
|
}
|
||
|
|
|
||
|
|
abs, err := filepath.Abs(filePath)
|
||
|
|
if err != nil {
|
||
|
|
return metainfo.Hash{}, fmt.Errorf("seed_file: resolve path: %w", err)
|
||
|
|
}
|
||
|
|
st, err := os.Stat(abs)
|
||
|
|
if err != nil {
|
||
|
|
return metainfo.Hash{}, fmt.Errorf("seed_file: stat: %w", err)
|
||
|
|
}
|
||
|
|
if st.IsDir() {
|
||
|
|
return metainfo.Hash{}, fmt.Errorf("seed_file: only single files are supported, %s is a directory", abs)
|
||
|
|
}
|
||
|
|
|
||
|
|
info := metainfo.Info{
|
||
|
|
PieceLength: chooseSeedPieceLength(st.Size()),
|
||
|
|
Name: filepath.Base(abs),
|
||
|
|
}
|
||
|
|
if err := info.BuildFromFilePath(abs); err != nil {
|
||
|
|
return metainfo.Hash{}, fmt.Errorf("seed_file: build info: %w", err)
|
||
|
|
}
|
||
|
|
infoBytes, err := bencode.Marshal(info)
|
||
|
|
if err != nil {
|
||
|
|
return metainfo.Hash{}, fmt.Errorf("seed_file: marshal info: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
mi := &metainfo.MetaInfo{
|
||
|
|
InfoBytes: infoBytes,
|
||
|
|
AnnounceList: makeAnnounceList(trackerURLs),
|
||
|
|
CreatedBy: "unarr-seed-file",
|
||
|
|
CreationDate: time.Now().Unix(),
|
||
|
|
}
|
||
|
|
ih := mi.HashInfoBytes()
|
||
|
|
|
||
|
|
t, err := client.AddTorrent(mi)
|
||
|
|
if err != nil {
|
||
|
|
return metainfo.Hash{}, fmt.Errorf("seed_file: add torrent: %w", err)
|
||
|
|
}
|
||
|
|
// Mark every piece as needed so the client treats us as a complete
|
||
|
|
// seeder right away — anacrolix's verifier will hash the file
|
||
|
|
// asynchronously and flip pieces to "have" as it goes.
|
||
|
|
t.DownloadAll()
|
||
|
|
|
||
|
|
return ih, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// makeAnnounceList shapes the tracker URL slice into the bencoded
|
||
|
|
// AnnounceList format anacrolix expects.
|
||
|
|
func makeAnnounceList(urls []string) metainfo.AnnounceList {
|
||
|
|
if len(urls) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
tier := make([]string, 0, len(urls))
|
||
|
|
for _, u := range urls {
|
||
|
|
if u == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
tier = append(tier, u)
|
||
|
|
}
|
||
|
|
if len(tier) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return metainfo.AnnounceList{tier}
|
||
|
|
}
|
||
|
|
|
||
|
|
// chooseSeedPieceLength picks the piece size for a single-file torrent
|
||
|
|
// based on the libtorrent / qBittorrent ladder. Mirrored from the
|
||
|
|
// wstracker-probe seeder so generated torrents are interoperable.
|
||
|
|
func chooseSeedPieceLength(size int64) int64 {
|
||
|
|
switch {
|
||
|
|
case size < 4*1024*1024:
|
||
|
|
return 16 * 1024
|
||
|
|
case size < 64*1024*1024:
|
||
|
|
return 64 * 1024
|
||
|
|
case size < 512*1024*1024:
|
||
|
|
return 256 * 1024
|
||
|
|
case size < 4*1024*1024*1024:
|
||
|
|
return 1024 * 1024
|
||
|
|
default:
|
||
|
|
return 4 * 1024 * 1024
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// SeedFileOnDownloader is a convenience wrapper that pulls the
|
||
|
|
// underlying anacrolix client out of a TorrentDownloader and forwards
|
||
|
|
// to SeedFile. trackerURLs default to the downloader's WebRTC
|
||
|
|
// trackers when nil/empty.
|
||
|
|
func SeedFileOnDownloader(d *TorrentDownloader, filePath string) (metainfo.Hash, error) {
|
||
|
|
if d == nil {
|
||
|
|
return metainfo.Hash{}, errors.New("seed_file: downloader is nil")
|
||
|
|
}
|
||
|
|
trackers := d.cfg.WebRTCTrackers
|
||
|
|
if !d.cfg.WebRTCEnabled {
|
||
|
|
// We could still build the torrent, but no browser would find
|
||
|
|
// it via the WSS tracker — bail loud so the operator notices.
|
||
|
|
return metainfo.Hash{}, errors.New("seed_file: WebRTC peer disabled in config; set [downloads.webrtc].enabled = true to use this feature")
|
||
|
|
}
|
||
|
|
return SeedFile(d.client, filePath, trackers)
|
||
|
|
}
|