diff --git a/server.go b/server.go index 1927cc2..e8a2806 100644 --- a/server.go +++ b/server.go @@ -69,6 +69,7 @@ func setup() { nameRe := regexp.MustCompile(`^/(?P[a-z0-9-\.]+)$`) selifRe := regexp.MustCompile(`^/selif/(?P[a-z0-9-\.]+)$`) selifIndexRe := regexp.MustCompile(`^/selif/$`) + torrentRe := regexp.MustCompile(`^/(?P[a-z0-9-\.]+)/torrent$`) goji.Get("/", indexHandler) @@ -84,6 +85,7 @@ func setup() { goji.Get(nameRe, fileDisplayHandler) goji.Get(selifRe, fileServeHandler) goji.Get(selifIndexRe, unauthorizedHandler) + goji.Get(torrentRe, fileTorrentHandler) goji.NotFound(notFoundHandler) } diff --git a/server_test.go b/server_test.go index fc8b7f8..f029e17 100644 --- a/server_test.go +++ b/server_test.go @@ -371,6 +371,15 @@ func TestPutAndDelete(t *testing.T) { if w.Code != 404 { t.Fatal("Status code was not 404, but " + strconv.Itoa(w.Code)) } + + // Make sure torrent is also gone + w = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/"+myjson.Filename+"/torrent", nil) + goji.DefaultMux.ServeHTTP(w, req) + + if w.Code != 404 { + t.Fatal("Status code was not 404, but " + strconv.Itoa(w.Code)) + } } func TestPutAndSpecificDelete(t *testing.T) { @@ -418,6 +427,15 @@ func TestPutAndSpecificDelete(t *testing.T) { if w.Code != 404 { t.Fatal("Status code was not 404, but " + strconv.Itoa(w.Code)) } + + // Make sure torrent is gone too + w = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/"+myjson.Filename+"/torrent", nil) + goji.DefaultMux.ServeHTTP(w, req) + + if w.Code != 404 { + t.Fatal("Status code was not 404, but " + strconv.Itoa(w.Code)) + } } func TestShutdown(t *testing.T) { diff --git a/templates/display/base.html b/templates/display/base.html index 94f49eb..2841d72 100644 --- a/templates/display/base.html +++ b/templates/display/base.html @@ -17,6 +17,7 @@ file expires in {{ expiry }} | {% endif %} {{ size }} | + torrent | get
diff --git a/torrent.go b/torrent.go new file mode 100644 index 0000000..5235c35 --- /dev/null +++ b/torrent.go @@ -0,0 +1,98 @@ +package main + +import ( + "bytes" + "crypto/sha1" + "fmt" + "io" + "net/http" + "os" + "path" + "time" + + "github.com/zeebo/bencode" + "github.com/zenazn/goji/web" +) + +const ( + TORRENT_PIECE_LENGTH = 262144 +) + +type TorrentInfo struct { + PieceLength int `bencode:"piece length"` + Pieces string `bencode:"pieces"` + Name string `bencode:"name"` + Length int `bencode:"length"` +} + +type Torrent struct { + Encoding string `bencode:"encoding"` + Info TorrentInfo `bencode:"info"` + UrlList []string `bencode:"url-list"` +} + +func hashPiece(piece []byte) []byte { + h := sha1.New() + h.Write(piece) + return h.Sum(nil) +} + +func CreateTorrent(fileName string, filePath string) ([]byte, error) { + chunk := make([]byte, TORRENT_PIECE_LENGTH) + + torrent := Torrent{ + Encoding: "UTF-8", + Info: TorrentInfo{ + PieceLength: TORRENT_PIECE_LENGTH, + Name: fileName, + }, + UrlList: []string{fmt.Sprintf("%sselif/%s", Config.siteURL, fileName)}, + } + + f, err := os.Open(filePath) + if err != nil { + return []byte{}, err + } + + for { + n, err := f.Read(chunk) + if err == io.EOF { + break + } else if err != nil { + return []byte{}, err + } + + torrent.Info.Length += n + torrent.Info.Pieces += string(hashPiece(chunk[:n])) + } + + f.Close() + + data, err := bencode.EncodeBytes(&torrent) + if err != nil { + return []byte{}, err + } + + return data, nil +} + +func fileTorrentHandler(c web.C, w http.ResponseWriter, r *http.Request) { + fileName := c.URLParams["name"] + filePath := path.Join(Config.filesDir, fileName) + + if !fileExistsAndNotExpired(fileName) { + notFoundHandler(c, w, r) + return + } + + encoded, err := CreateTorrent(fileName, filePath) + if err != nil { + oopsHandler(c, w, r) // 500 - creating torrent failed + return + } + + w.Header().Set(`Content-Disposition`, fmt.Sprintf(`attachment; filename="%s.torrent"`, fileName)) + http.ServeContent(w, r, "", time.Now(), bytes.NewReader(encoded)) +} + +// vim:set ts=8 sw=8 noet: diff --git a/torrent_test.go b/torrent_test.go new file mode 100644 index 0000000..5a1d34a --- /dev/null +++ b/torrent_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "testing" + + "github.com/zeebo/bencode" +) + +func TestCreateTorrent(t *testing.T) { + fileName := "server.go" + var decoded Torrent + + encoded, err := CreateTorrent(fileName, fileName) + if err != nil { + t.Fatal(err) + } + + bencode.DecodeBytes(encoded, &decoded) + + if decoded.Encoding != "UTF-8" { + t.Fatalf("Encoding was %s, expected UTF-8", decoded.Encoding) + } + + if decoded.Info.Name != "server.go" { + t.Fatalf("Name was %s, expected server.go", decoded.Info.Name) + } + + if decoded.Info.PieceLength <= 0 { + t.Fatal("Expected a piece length, got none") + } + + if len(decoded.Info.Pieces) <= 0 { + t.Fatal("Expected at least one piece, got none") + } + + if decoded.Info.Length <= 0 { + t.Fatal("Length was less than or equal to 0, expected more") + } + + tracker := fmt.Sprintf("%sselif/%s", Config.siteURL, fileName) + if decoded.UrlList[0] != tracker { + t.Fatalf("First entry in URL list was %s, expected %s", decoded.UrlList[0], tracker) + } +} + +func TestCreateTorrentWithImage(t *testing.T) { + var decoded Torrent + + encoded, err := CreateTorrent("test.jpg", "static/images/404.jpg") + if err != nil { + t.Fatal(err) + } + + bencode.DecodeBytes(encoded, &decoded) + + if decoded.Info.Pieces != "r\x01\x80j\x99\x84\n\xd3dZ;1NX\xec;\x9d$+f" { + t.Fatal("Torrent pieces did not match expected pieces for image") + } +} + +// vim:set ts=8 sw=8 noet: