diff --git a/cmd/magneticow/api.go b/cmd/magneticow/api.go index fb96634..8728b00 100644 --- a/cmd/magneticow/api.go +++ b/cmd/magneticow/api.go @@ -1,18 +1,163 @@ package main import ( + "bytes" "encoding/hex" "encoding/json" "fmt" + "io" + "io/ioutil" "net/http" + "os" "strconv" + "strings" "time" - "github.com/boramalper/magnetico/pkg/persistence" + "golang.org/x/text/encoding/charmap" + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/storage" "github.com/gorilla/mux" "go.uber.org/zap" + + "github.com/boramalper/magnetico/pkg/persistence" ) +type ApiReadmeHandler struct { + client *torrent.Client + tempdir string +} + +func NewApiReadmeHandler() (*ApiReadmeHandler, error) { + h := new(ApiReadmeHandler) + var err error + + h.tempdir, err = ioutil.TempDir("", "magneticod_") + if err != nil { + return nil, err + } + + config := torrent.NewDefaultClientConfig() + config.ListenPort = 0 + config.DefaultStorage = storage.NewFileByInfoHash(h.tempdir) + + h.client, err = torrent.NewClient(config) + if err != nil { + _ = os.RemoveAll(h.tempdir) + return nil, err + } + + return h, nil +} + +func (h *ApiReadmeHandler) Close() { + h.client.Close() + _ = os.RemoveAll(h.tempdir) +} + +func (h *ApiReadmeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + infohashHex := mux.Vars(r)["infohash"] + + infohash, err := hex.DecodeString(infohashHex) + if err != nil { + respondError(w, http.StatusBadRequest, "couldn't decode infohash: %s", err.Error()) + return + } + + files, err := database.GetFiles(infohash) + if err != nil { + zap.L().Error("GetFiles error", zap.Error(err)) + respondError(w, http.StatusInternalServerError, "Internal Server Error") + } + + ok := false + for _, file := range files { + if strings.HasSuffix(file.Path, ".nfo") { + ok = true + break + } else if strings.Contains(file.Path, "read") { + ok = true + break + } + } + + if !ok { + w.WriteHeader(http.StatusNotFound) + return + } + + zap.L().Warn("README") + + t, err := h.client.AddMagnet("magnet:?xt=urn:btih:" + infohashHex) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + defer t.Drop() + + zap.L().Warn("WAITING FOR INFO") + + select { + case <- t.GotInfo(): + + case <- time.After(30 * time.Second): + respondError(w, http.StatusInternalServerError, "Timeout") + return + } + + zap.L().Warn("GOT INFO!") + + t.CancelPieces(0, t.NumPieces()) + + var file *torrent.File + for _, file = range t.Files() { + filePath := file.Path() + if strings.HasSuffix(filePath, ".nfo") { + break + } else if strings.Contains(filePath, "read") { + break + } + } + + if file == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + // Cancel if the file is larger than 50 KiB + if file.Length() > 50 * 1024 { + w.WriteHeader(http.StatusRequestEntityTooLarge) + return + } + + file.Download() + + reader := file.NewReader() + // BEWARE: + // ioutil.ReadAll(reader) + // returns some adjancent garbage too, for reasons unknown... + content := make([]byte, file.Length()) + _, err = io.ReadFull(reader, content) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + defer reader.Close() + + if strings.HasSuffix(file.Path(), ".nfo") { + content, err = charmap.CodePage437.NewDecoder().Bytes(content) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + } + + // Because .nfo files are right padded with \x00'es. + content = bytes.TrimRight(content, "\x00") + + w.Header().Set("Content-Type", "text/plain;charset=UTF-8") + _, _ = w.Write(content) +} + func apiTorrents(w http.ResponseWriter, r *http.Request) { // @lastOrderedValue AND @lastID are either both supplied or neither of them should be supplied // at all; and if that is NOT the case, then return an error. diff --git a/cmd/magneticow/data/static/fonts/PxPlus_IBM_VGA8/README.md b/cmd/magneticow/data/static/fonts/PxPlus_IBM_VGA8/README.md new file mode 100644 index 0000000..990db3d --- /dev/null +++ b/cmd/magneticow/data/static/fonts/PxPlus_IBM_VGA8/README.md @@ -0,0 +1,7 @@ +# PxPlus_IBM_VGA8 + +**Source:** https://int10h.org/oldschool-pc-fonts/ released under the +[CC BY-SA 4.0 license](http://creativecommons.org/licenses/by-sa/4.0/). + +.woff is generated by [Font Squirrel](https://www.fontsquirrel.com/tools/webfont-generator) from +`PxPlus_IBM_VGA8.ttf`. diff --git a/cmd/magneticow/data/static/fonts/PxPlus_IBM_VGA8/pxplus_ibm_vga8-webfont.woff b/cmd/magneticow/data/static/fonts/PxPlus_IBM_VGA8/pxplus_ibm_vga8-webfont.woff new file mode 100644 index 0000000..1533225 Binary files /dev/null and b/cmd/magneticow/data/static/fonts/PxPlus_IBM_VGA8/pxplus_ibm_vga8-webfont.woff differ diff --git a/cmd/magneticow/data/static/scripts/common.js b/cmd/magneticow/data/static/scripts/common.js index 7c7a0bf..a0625b4 100644 --- a/cmd/magneticow/data/static/scripts/common.js +++ b/cmd/magneticow/data/static/scripts/common.js @@ -32,6 +32,22 @@ function humaniseDate(unixTime) { }); } +// a fetch() that errs on anything but HTTP 2XX +// Source: https://github.com/github/fetch/issues/155#issuecomment-108288863 +function myFetch(url, options) { + if (options == null) options = {} + if (options.credentials == null) options.credentials = 'same-origin' + return fetch(url, options).then(function(response) { + if (response.status >= 200 && response.status < 300) { + return Promise.resolve(response) + } else { + var error = new Error(response.statusText || response.status) + error.response = response + return Promise.reject(error) + } + }) +} + /** * Returns the ISO 8601 week number for this date. * diff --git a/cmd/magneticow/data/static/scripts/torrent.js b/cmd/magneticow/data/static/scripts/torrent.js index 4be0c8d..23a2539 100644 --- a/cmd/magneticow/data/static/scripts/torrent.js +++ b/cmd/magneticow/data/static/scripts/torrent.js @@ -8,7 +8,7 @@ "use strict"; -window.onload = function() { +window.onload = function () { let infoHash = window.location.pathname.split("/")[2]; fetch("/api/v0.1/torrents/" + infoHash).then(x => x.json()).then(x => { @@ -16,7 +16,7 @@ window.onload = function() { const template = document.getElementById("main-template").innerHTML; document.querySelector("main").innerHTML = Mustache.render(template, { - name: x.name, + name: x.name, infoHash: x.infoHash, sizeHumanised: fileSize(x.size), discoveredOnHumanised: humaniseDate(x.discoveredOn), @@ -35,11 +35,24 @@ window.onload = function() { tree.add({ id: pathElems.slice(0, i + 1).join("/"), parent: i >= 1 ? pathElems.slice(0, i).join("/") : undefined, - label: pathElems[i] + ( i === pathElems.length - 1 ? " " + fileSize(e.size) + "" : ""), + label: pathElems[i] + (i === pathElems.length - 1 ? " " + fileSize(e.size) + "" : ""), opened: true, }); } } }); + + myFetch("/api/v0.1/torrents/" + infoHash + "/readme") + .then(response => { + return response.text(); + }) + .then(x => { + const readme = document.getElementById("readme"); + readme.innerText = x; + }) + .catch(err => { + const readme = document.getElementById("readme"); + readme.innerText = err; + }); }); }; diff --git a/cmd/magneticow/data/static/styles/torrent.css b/cmd/magneticow/data/static/styles/torrent.css index 610ac6c..563b23d 100644 --- a/cmd/magneticow/data/static/styles/torrent.css +++ b/cmd/magneticow/data/static/styles/torrent.css @@ -77,4 +77,19 @@ th { tt { font-style: italic; font-size: smaller; -} \ No newline at end of file +} + +@font-face { + font-family: 'PxPlus-IBM-VGA8'; + src: URL('/static/fonts/PxPlus_IBM_VGA8/pxplus_ibm_vga8-webfont.woff') format('woff'); +} + +#readme { + padding: 1em; + display: inline-block; + background-color: black; + color: white; + font-family: 'PxPlus-IBM-VGA8', monospace; + line-height: 1em; + letter-spacing: -0.5px; +} diff --git a/cmd/magneticow/data/templates/torrent.html b/cmd/magneticow/data/templates/torrent.html index 102a2ee..444aa20 100644 --- a/cmd/magneticow/data/templates/torrent.html +++ b/cmd/magneticow/data/templates/torrent.html @@ -13,8 +13,6 @@ - - @@ -56,5 +57,8 @@
+ + + \ No newline at end of file diff --git a/cmd/magneticow/main.go b/cmd/magneticow/main.go index fd05785..a539c02 100644 --- a/cmd/magneticow/main.go +++ b/cmd/magneticow/main.go @@ -102,6 +102,12 @@ func main() { } }() + apiReadmeHandler, err := NewApiReadmeHandler() + if err != nil { + zap.L().Fatal("Could not initialise readme handler", zap.Error(err)) + } + defer apiReadmeHandler.Close() + router := mux.NewRouter() router.HandleFunc("/", BasicAuth(rootHandler, "magneticow")) @@ -114,6 +120,8 @@ func main() { BasicAuth(apiTorrent, "magneticow")) router.HandleFunc("/api/v0.1/torrents/{infohash:[a-f0-9]{40}}/filelist", BasicAuth(apiFilelist, "magneticow")) + router.Handle("/api/v0.1/torrents/{infohash:[a-f0-9]{40}}/readme", + apiReadmeHandler) router.HandleFunc("/feed", BasicAuth(feedHandler, "magneticow")) @@ -171,7 +179,6 @@ func main() { templates["feed"] = template.Must(template.New("feed").Funcs(templateFunctions).Parse(string(mustAsset("templates/feed.xml")))) templates["homepage"] = template.Must(template.New("homepage").Funcs(templateFunctions).Parse(string(mustAsset("templates/homepage.html")))) - var err error database, err = persistence.MakeDatabase(opts.Database, logger) if err != nil { zap.L().Fatal("could not access to database", zap.Error(err)) @@ -190,7 +197,7 @@ func main() { // TODO: I think there is a standard lib. function for this func respondError(w http.ResponseWriter, statusCode int, format string, a ...interface{}) { w.WriteHeader(statusCode) - w.Write([]byte(fmt.Sprintf(format, a...))) + _, _ = w.Write([]byte(fmt.Sprintf(format, a...))) } func mustAsset(name string) []byte { diff --git a/go.mod b/go.mod index f384208..adca2b7 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( go.uber.org/zap v1.10.0 golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f golang.org/x/sys v0.0.0-20190516110030-61b9204099cb + golang.org/x/text v0.3.0 ) go 1.13