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 @@