commit 1e36c858805afbc50e4be9c0d2a0328d2bf16d65 Author: mappu Date: Sun Aug 13 13:57:08 2017 +1200 initial commit diff --git a/ArchiveServer.go b/ArchiveServer.go new file mode 100644 index 0000000..8436d6f --- /dev/null +++ b/ArchiveServer.go @@ -0,0 +1,41 @@ +package archive + +import ( + "fmt" + "time" +) + +const ( + SERVER_VERSION = "archive/3.0" +) + +type ArchiveServer struct { + timezone *time.Location + cfg *Config +} + +func NewArchiveServer(cfg *Config) (*ArchiveServer, error) { + tz, err := time.LoadLocation(cfg.Timezone) + if err != nil { + return nil, err + } + + if cfg.LinesPerPage <= 0 { + return nil, fmt.Errorf("Invalid %d lines per page", cfg.LinesPerPage) + } + + if len(cfg.Logs) == 0 { + return nil, fmt.Errorf("No log sources configured") + } + + for _, ls := range cfg.Logs { + if len(ls.FileLocation) == 0 { + return nil, fmt.Errorf(`No file locations for log source "%s"`, ls.Description) + } + } + + return &ArchiveServer{ + timezone: tz, + cfg: cfg, + }, nil +} diff --git a/Config.go b/Config.go new file mode 100644 index 0000000..0488eec --- /dev/null +++ b/Config.go @@ -0,0 +1,58 @@ +package archive + +import ( + "time" +) + +type YearMonth struct { + Year int + + // time.Month is a 1-based month counting system + Month time.Month +} + +type LogLocation struct { + + // LogFilePath will be passed to time.Format(). + LogFilePath string + + StartMonth YearMonth + + // Leave null to use a current/ongoing log + EndMonth *YearMonth +} + +type LogSource struct { + Description string + + // Short strings by which this source can be identified. If any slugs are + // present, the first slug will be used as the canonical URL. + Slugs []string + + // Disk location of logs for this source, for each partial date range + FileLocation []LogLocation +} + +func (this *LogSource) LatestDate() YearMonth { + // assume it's the last in the list (although it might not be) + end := this.FileLocation[len(this.FileLocation)-1].EndMonth + if end == nil { + return YearMonth{time.Now().Year(), time.Now().Month()} + } else { + return *end + } +} + +type Config struct { + Timezone string + LinesPerPage int + Logs []LogSource +} + +func NewConfig() *Config { + return &Config{ + Timezone: time.Local.String(), + LinesPerPage: 100, + Logs: make([]LogSource, 0), + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..468f3e8 --- /dev/null +++ b/Makefile @@ -0,0 +1,73 @@ +# +# Makefile for archive +# + +VERSION:=3.0.0 + +SOURCES:=Makefile \ + static \ + cmd $(wildcard cmd/archive-server/*.go) \ + $(wildcard *.go) + +GOFLAGS := -ldflags='-s -w' -gcflags='-trimpath=$(GOPATH)' -asmflags='-trimpath=$(GOPATH)' + +# +# Phony targets +# + +.PHONY: all dist clean + +all: build/linux64/archive-server build/win32/archive-server.exe + +dist: \ + _dist/archive-$(VERSION)-linux64.tar.gz \ + _dist/archive-$(VERSION)-win32.7z \ + _dist/archive-$(VERSION)-src.zip + +clean: + if [ -f ./staticResources.go ] ; then rm ./staticResources.go ; fi + if [ -d ./build ] ; then rm -r ./build ; fi + if [ -f ./archive ] ; then rm ./archive ; fi + +# +# Generated files +# + +staticResources.go: static/ static/* + go-bindata -o staticResources.go -prefix static -pkg archive static + + +# +# Release artefacts +# + +build/linux64/archive-server: $(SOURCES) staticResources.go + mkdir -p build/linux64 + (cd cmd/archive-server ; \ + CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \ + go build $(GOFLAGS) -o ../../build/linux64/archive-server \ + ) + +build/win32/archive-server.exe: $(SOURCES) staticResources.go + mkdir -p build/win32 + (cd cmd/archive-server ; \ + PATH=/usr/lib/mxe/usr/bin:$(PATH) CC=i686-w64-mingw32.static-gcc \ + CGO_ENABLED=1 GOOS=windows GOARCH=386 \ + go build $(GOFLAGS) -o ../../build/win32/archive-server.exe \ + ) + +_dist/archive-$(VERSION)-linux64.tar.gz: build/linux64/archive-server + mkdir -p _dist + tar caf _dist/archive-$(VERSION)-linux64.tar.gz -C build/linux64 archive-server --owner=0 --group=0 + +_dist/archive-$(VERSION)-win32.7z: build/win32/archive-server.exe + mkdir -p _dist + ( cd build/win32 ; \ + if [ -f dist.7z ] ; then rm dist.7z ; fi ; \ + 7z a dist.7z archive-server.exe ; \ + mv dist.7z ../../_dist/archive-$(VERSION)-win32.7z \ + ) + +_dist/archive-$(VERSION)-src.zip: $(SOURCES) + git archive --format=zip HEAD > _dist/archive-$(VERSION)-src.zip + diff --git a/Router.go b/Router.go new file mode 100644 index 0000000..a63405b --- /dev/null +++ b/Router.go @@ -0,0 +1,101 @@ +package archive + +import ( + "fmt" + "net/http" + "net/url" + "strconv" +) + +func (this *ArchiveServer) lookupSourceByNumericString(slug string) *LogSource { + intval, err := strconv.Atoi(slug) + if err != nil { + return nil + } + + if intval >= 0 && intval < len(this.cfg.Logs) { + return &this.cfg.Logs[intval] + } + + return nil +} + +func (this *ArchiveServer) lookupSource(slug string) *LogSource { + if src := this.lookupSourceByNumericString(slug); src != nil { + return src + } + + for i, ls := range this.cfg.Logs { + for _, s := range ls.Slugs { + if s == slug { + return &this.cfg.Logs[i] + } + } + } + + return nil // not found +} + +func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Server", SERVER_VERSION) + + if r.Method != "GET" { + http.Error(w, "Expected GET", http.StatusMethodNotAllowed) + return + } + + if len(r.URL.Query().Get("y")) > 0 || len(r.URL.Query().Get("q")) > 0 || len(r.URL.Query().Get("h")) > 0 { + this.legacyRoute(w, r) + + } else { + // TODO + } +} + +func (this *ArchiveServer) legacyRoute(w http.ResponseWriter, r *http.Request) { + + intval := func(sz string) int { + ret, _ := strconv.Atoi(sz) + return ret + } + get := r.URL.Query().Get + hasGet := func(sz string) bool { + return len(get(sz)) > 0 + } + redirectf := func(format string, a ...interface{}) { + http.Redirect(w, r, fmt.Sprintf(format, a...), http.StatusTemporaryRedirect) + } + + // + + hubid := 0 + if hasGet("h") { + hubid = intval(get("h")) + } + + if hasGet("q") { + if hasGet("rx") { + redirectf(`/%d/rx/%s`, hubid, url.QueryEscape(get("q"))) + } else { + redirectf(`/%d/search/%s`, hubid, url.QueryEscape(get("q"))) + } + + } else if hasGet("y") && hasGet("m") { + year := intval(get("y")) + month := intval(get("m")) + if hasGet("p") { + redirectf(`/%d/%d/%d/page-%d`, hubid, year, month, intval(get("p"))) + } else { + redirectf(`/%d/%d/%d`, hubid, year, month) + } + + } else { + if hi := this.lookupSourceByNumericString(get("h")); hi != nil { + currentDate := hi.LatestDate() + redirectf(`/%d/%d/%d`, hubid, currentDate.Year, currentDate.Month) + } else { + redirectf(`/`) + } + + } +} diff --git a/cmd/archive-server/main.go b/cmd/archive-server/main.go new file mode 100644 index 0000000..f80fd79 --- /dev/null +++ b/cmd/archive-server/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/json" + "flag" + "io/ioutil" + "log" + "net/http" + + "code.ivysaur.me/archive" +) + +func main() { + bindAddr := flag.String("listen", "127.0.0.1:80", "Bind address") + configPath := flag.String("config", "config.json", "Configuration file") + flag.Parse() + + // + + cfg, err := ioutil.ReadFile(*configPath) + if err != nil { + log.Fatalf("Failed to load configuration file '%s': %s\n", *configPath, err.Error()) + } + + log.Printf("Loading configuration from '%s'...\n", *configPath) + opts := archive.NewConfig() + err = json.Unmarshal(cfg, &opts) + if err != nil { + log.Fatalf("Failed to parse configuration file: %s\n", err.Error()) + } + + // + + as, err := archive.NewArchiveServer(opts) + if err != nil { + log.Fatal(err.Error()) + } + + log.Printf("Starting archive server on '%s'...", *bindAddr) + err = http.ListenAndServe(*bindAddr, as) + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/static/archive.js b/static/archive.js new file mode 100755 index 0000000..91e04a0 --- /dev/null +++ b/static/archive.js @@ -0,0 +1,176 @@ + +/* mousetrap v1.4.6 craig.is/killing/mice */ +(function(J,r,f){function s(a,b,d){a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent("on"+b,d)}function A(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return h[a.which]?h[a.which]:B[a.which]?B[a.which]:String.fromCharCode(a.which).toLowerCase()}function t(a){a=a||{};var b=!1,d;for(d in n)a[d]?b=!0:n[d]=0;b||(u=!1)}function C(a,b,d,c,e,v){var g,k,f=[],h=d.type;if(!l[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(g=0;gg||h.hasOwnProperty(g)&&(p[h[g]]=g)}e=p[d]?"keydown":"keypress"}"keypress"==e&&f.length&&(e="keydown");return{key:c,modifiers:f,action:e}}function F(a,b,d,c,e){q[a+":"+d]=b;a=a.replace(/\s+/g," ");var f=a.split(" ");1":".","?":"/","|":"\\"},G={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p,l={},q={},n={},D,z=!1,I=!1,u=!1;for(f=1;20>f;++f)h[111+f]="f"+f;for(f=0;9>=f;++f)h[f+96]=f;s(r,"keypress",y);s(r,"keydown",y);s(r,"keyup",y);var m={bind:function(a,b,d){a=a instanceof Array?a:[a];for(var c=0;c$2") + .replace( + /\[(\d\d\d\d-\d\d-\d\d)\s(\d\d\:\d\d)\:(\d\d)\]/g, + '[$1 $2:$3]' + ) + .replace(/(\[[0-9:\-\s]*?\])/g, '$1') + .replace(/(\<\;[^\s]+?\>\;)/g, "$1") + .replace(/(\*\*\*.+)/g, "$1") + .replace(/(\>\;imp[^\n\r\<]*)/g, "$1") + .replace(/(https?:\/\/.+?)([\s|<])/g, "$1$2") + .replace(/magnet:\?.+dn=([^\< ]+)/g, function(match, m1) { + return "[MAGNET] " + urldesc(m1) + ""; + }) + ; +} + +function fontSize(change) { + var curSize = cookie_get("fontsize"); + if (curSize === null) { + curSize = 12; + } else { + curSize = + curSize; + } + + curSize += change; + + cookie_set("fontsize", curSize); + + i("chatarea").style["fontSize"] = ""+curSize+"px"; +} + +function toggleMenu() { + t(i("tr1")); + t(i("tr2")); + t(i("spm")); +} + +function highlightLine(no) { + var lines = i('chatarea').innerHTML.split('
'); + + lines[no] = '' + lines[no] + ''; + + i('chatarea').innerHTML = lines.join('
'); +} + +var alreadyLoaded = false; + +function onLoad() { + + if (alreadyLoaded) { + return; + } + alreadyLoaded = true; + + // + + i('chatarea').innerHTML = highlight(i('chatarea').innerHTML); + + // + + if ( + ! /\/search\//.test(window.location.pathname) && + document.location.hash.substr(0, 6) === '#line-' + ) { + highlightLine( parseInt(document.location.hash.substr(6), 10) ); + document.location.hash = ''; + } + + // + + fontSize(0); + + // + + i('selHub').onchange = function() { + if ( /\/search\//.test(window.location.pathname) ) { + window.location.pathname = i('selHub').value + "/search/" + encodeURIComponent( i('searchbox').value ); + + } else if ( /\/rx\//.test(window.location.pathname) ) { + window.location.pathname = i('selHub').value + "/rx/" + encodeURIComponent( i('searchbox').value ) + + } else { + i('frmHub').submit(); + } + }; + + // + + Mousetrap.bind('ctrl+alt+h', function() { i("pgprev").click(); }); + Mousetrap.bind('ctrl+alt+l', function() { i("pgnext").click(); }); + Mousetrap.bind('ctrl+alt+j', function() { + YMgoto( YMmod(i("seldate").value, 1) ) + }); + Mousetrap.bind('ctrl+alt+k', function() { + YMgoto( YMmod(i("seldate").value, -1) ) + }); + + Mousetrap.bind('ctrl+alt+m', function() { + i("searchbox").focus(); + }); + + // + +} + +function YMmod(str, change) { + var t = str.split('-').map(function(x) { return +x; }); + t[1] += change; + if (t[1] == 13) { + t[0] += 1; + t[1] = 1; + } + if (t[1] == 0) { + t[0] -= 1; + t[1] = 12; + } + return t.join('-'); +} + +function YMgoto(str) { + var t = str.split("-"); + i("f_y").value = t[0]; + i("f_m").value = t[1]; + i("seldate").form.submit(); +} + +function setYM(el) { + YMgoto(el.value); +} + +window.addEventListener('load', onLoad); diff --git a/static/index.php b/static/index.php new file mode 100755 index 0000000..7823bc6 --- /dev/null +++ b/static/index.php @@ -0,0 +1,16 @@ +