Skip to content

Commit 3c43027

Browse files
committed
cmd/tlog-torrent: experimental tool to generate .torrent files
1 parent 2f633df commit 3c43027

File tree

3 files changed

+243
-0
lines changed

3 files changed

+243
-0
lines changed

cmd/tlog-torrent/main.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"io/fs"
7+
"log/slog"
8+
"os"
9+
"slices"
10+
"strings"
11+
"time"
12+
13+
"filippo.io/sunlight"
14+
"filippo.io/sunlight/internal/torrent"
15+
"filippo.io/torchwood"
16+
"golang.org/x/mod/sumdb/tlog"
17+
)
18+
19+
func main() {
20+
if len(os.Args) < 3 {
21+
slog.Error("usage: tlog-torrent <path> <monitoring prefix>")
22+
return
23+
}
24+
25+
monitoringPrefix := strings.TrimSuffix(os.Args[2], "/")
26+
27+
root, err := os.OpenRoot(os.Args[1])
28+
if err != nil {
29+
slog.Error("failed to open root", "error", err)
30+
return
31+
}
32+
33+
checkpointBytes, err := fs.ReadFile(root.FS(), "checkpoint")
34+
if err != nil {
35+
slog.Error("failed to read checkpoint", "error", err)
36+
return
37+
}
38+
checkpointBytes = checkpointBytes[:bytes.Index(checkpointBytes, []byte("\n\n"))+1]
39+
checkpoint, err := torchwood.ParseCheckpoint(string(checkpointBytes))
40+
if err != nil {
41+
slog.Error("failed to parse checkpoint", "error", err, "checkpoint", string(checkpointBytes))
42+
return
43+
}
44+
45+
domain, path, _ := strings.Cut(checkpoint.Origin, "/")
46+
parts := strings.Split(domain, ".")
47+
slices.Reverse(parts)
48+
parts = append(parts, strings.Split(path, "/")...)
49+
reverse := strings.Join(parts, ".")
50+
51+
h := torrent.NewPieceHash(524288)
52+
t := torrent.NewWriter(os.Stdout)
53+
t.WriteDict(func(t *torrent.Writer) {
54+
t.WriteString("comment")
55+
t.WriteBytes(checkpointBytes)
56+
t.WriteString("created by")
57+
t.WriteString("tlog-torrent")
58+
t.WriteString("creation date")
59+
t.WriteInt64(time.Now().Unix())
60+
t.WriteString("info")
61+
t.WriteDict(func(t *torrent.Writer) {
62+
t.WriteString("collections")
63+
t.WriteList(func(t *torrent.Writer) {
64+
t.WriteString(reverse)
65+
})
66+
t.WriteString("files")
67+
t.WriteList(func(t *torrent.Writer) {
68+
for n := range checkpoint.N / sunlight.TileWidth {
69+
tile := sunlight.TilePath(tlog.Tile{
70+
H: sunlight.TileHeight,
71+
L: -1, N: n,
72+
W: sunlight.TileWidth,
73+
})
74+
f, err := root.Open(tile)
75+
if err != nil {
76+
slog.Error("failed to open tile", "tile", tile, "error", err)
77+
os.Exit(1)
78+
}
79+
length, err := io.Copy(h, f)
80+
if err != nil {
81+
slog.Error("failed to hash tile", "tile", tile, "error", err)
82+
os.Exit(1)
83+
}
84+
if err := f.Close(); err != nil {
85+
slog.Error("failed to close tile", "tile", tile, "error", err)
86+
os.Exit(1)
87+
}
88+
t.WriteDict(func(t *torrent.Writer) {
89+
t.WriteString("length")
90+
t.WriteInt64(length)
91+
t.WriteString("path")
92+
t.WriteList(func(t *torrent.Writer) {
93+
p := strings.TrimPrefix(tile, "tile/data/")
94+
for _, part := range strings.Split(p, "/") {
95+
t.WriteString(part)
96+
}
97+
})
98+
})
99+
}
100+
})
101+
t.WriteString("name")
102+
t.WriteString("data")
103+
t.WriteString("piece length")
104+
t.WriteInt(524288)
105+
t.WriteString("pieces")
106+
t.WriteBytes(h.Pieces())
107+
t.WriteString("update-url")
108+
t.WriteString(monitoringPrefix + "/tile-data.torrent")
109+
})
110+
t.WriteString("title")
111+
t.WriteString(checkpoint.Origin)
112+
t.WriteString("url-list")
113+
t.WriteList(func(t *torrent.Writer) {
114+
t.WriteString(monitoringPrefix + "/tile/")
115+
})
116+
})
117+
}

internal/torrent/bencode.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package torrent
2+
3+
import (
4+
"fmt"
5+
"io"
6+
)
7+
8+
type Writer struct {
9+
w io.Writer
10+
isDict bool
11+
isKey bool
12+
}
13+
14+
func NewWriter(w io.Writer) *Writer {
15+
return &Writer{w: w}
16+
}
17+
18+
func (w *Writer) WriteString(s string) {
19+
fmt.Fprintf(w.w, "%d:%s", len(s), s)
20+
if w.isKey {
21+
w.isKey = false
22+
} else if w.isDict {
23+
w.isKey = true
24+
}
25+
}
26+
27+
func (w *Writer) WriteBytes(b []byte) {
28+
fmt.Fprintf(w.w, "%d:", len(b))
29+
w.w.Write(b)
30+
if w.isKey {
31+
w.isKey = false
32+
} else if w.isDict {
33+
w.isKey = true
34+
}
35+
}
36+
37+
func (w *Writer) WriteInt(i int) {
38+
if w.isKey {
39+
panic("int can't be a key")
40+
}
41+
fmt.Fprintf(w.w, "i%de", i)
42+
if w.isDict {
43+
w.isKey = true
44+
}
45+
}
46+
47+
func (w *Writer) WriteInt64(i int64) {
48+
if w.isKey {
49+
panic("int can't be a key")
50+
}
51+
fmt.Fprintf(w.w, "i%de", i)
52+
if w.isDict {
53+
w.isKey = true
54+
}
55+
}
56+
57+
func (w *Writer) WriteList(f func(*Writer)) {
58+
if w.isKey {
59+
panic("list can't be a key")
60+
}
61+
fmt.Fprintf(w.w, "l")
62+
f(&Writer{w: w.w})
63+
fmt.Fprintf(w.w, "e")
64+
if w.isDict {
65+
w.isKey = true
66+
}
67+
}
68+
69+
func (w *Writer) WriteDict(f func(*Writer)) {
70+
if w.isKey {
71+
panic("dict can't be a key")
72+
}
73+
fmt.Fprintf(w.w, "d")
74+
ww := &Writer{}
75+
ww.isDict = true
76+
ww.isKey = true
77+
ww.w = w.w
78+
f(ww)
79+
if !ww.isKey {
80+
panic("missing value for key")
81+
}
82+
fmt.Fprintf(w.w, "e")
83+
if w.isDict {
84+
w.isKey = true
85+
}
86+
}

internal/torrent/files.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package torrent
2+
3+
import (
4+
"crypto/sha1"
5+
"hash"
6+
)
7+
8+
type PieceHash struct {
9+
size int
10+
idx int
11+
h hash.Hash
12+
pieces []byte
13+
}
14+
15+
func NewPieceHash(size int) *PieceHash {
16+
return &PieceHash{size: size, h: sha1.New()}
17+
}
18+
19+
func (ph *PieceHash) Write(b []byte) (int, error) {
20+
in := len(b)
21+
for len(b) > 0 {
22+
n := min(len(b), ph.size-ph.idx)
23+
ph.h.Write(b[:n])
24+
b = b[n:]
25+
ph.idx += n
26+
if ph.idx == ph.size {
27+
ph.pieces = ph.h.Sum(ph.pieces)
28+
ph.h.Reset()
29+
ph.idx = 0
30+
}
31+
}
32+
return in, nil
33+
}
34+
35+
func (ph *PieceHash) Pieces() []byte {
36+
if ph.idx > 0 {
37+
return ph.h.Sum(ph.pieces[:len(ph.pieces):len(ph.pieces)])
38+
}
39+
return ph.pieces
40+
}

0 commit comments

Comments
 (0)