Base project to template

master
bel 2020-03-12 13:48:04 -06:00
commit f022b26987
19 changed files with 543 additions and 0 deletions

10
.gitignore vendored Executable file
View File

@ -0,0 +1,10 @@
gollum
public
**.sw*
**/**.sw*
*.sw*
**/*.sw*
notes-server
exec-notes-server
firestormy
exec-firestormy

53
config/config.go Executable file
View File

@ -0,0 +1,53 @@
package config
import (
"fmt"
"local/args"
"local/storage"
"os"
"strings"
)
var (
Port string
OAuthServer string
Store storage.DB
StoreType string
StoreAddr string
StoreUser string
StorePass string
)
func init() {
Refresh()
}
func Refresh() {
if strings.Contains(fmt.Sprint(os.Args), " -test") {
return
}
as := args.NewArgSet()
as.Append(args.STRING, "port", "port to listen on", "49809")
as.Append(args.STRING, "oauth", "oauth URL", "")
as.Append(args.STRING, "storetype", "storage type", "map")
as.Append(args.STRING, "storeaddr", "storage address", "")
as.Append(args.STRING, "storeuser", "storage username", "")
as.Append(args.STRING, "storepass", "storage password", "")
if err := as.Parse(); err != nil {
panic(err)
}
Port = ":" + strings.TrimPrefix(as.Get("port").GetString(), ":")
OAuthServer = as.Get("oauth").GetString()
StoreType = as.Get("storetype").GetString()
StoreAddr = as.Get("storeaddr").GetString()
StoreUser = as.Get("storeuser").GetString()
StorePass = as.Get("storepass").GetString()
if db, err := storage.New(storage.TypeFromString(StoreType), StoreAddr, StoreUser, StorePass); err != nil {
panic(err)
} else {
Store = db
}
}

57
config/config_test.go Normal file
View File

@ -0,0 +1,57 @@
package config
import (
"io/ioutil"
"os"
"testing"
)
func TestConfigRefresh(t *testing.T) {
was := os.Args
defer func() {
os.Args = was
}()
os.Args = []string{"na"}
d, err := ioutil.TempDir(os.TempDir(), "firestormy.config.test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(d)
os.Setenv("PORT", "11111")
os.Setenv("OAUTH", "localhost:27777")
os.Setenv("STORETYPE", "leveldb")
os.Setenv("STOREADDR", d)
os.Setenv("STOREUSER", "user")
os.Setenv("STOREPASS", "pass")
Refresh()
if v := Port; v != ":11111" {
t.Error(v)
}
if v := OAuthServer; v != "localhost:27777" {
t.Error(v)
}
if v := StoreType; v != "leveldb" {
t.Error(v)
}
if v := StoreAddr; v != d {
t.Error(v)
}
if v := StoreUser; v != "user" {
t.Error(v)
}
if v := StorePass; v != "pass" {
t.Error(v)
}
if err := Store.Set("key", []byte("value")); err != nil {
t.Error(err)
} else if b, err := Store.Get("key"); err != nil {
t.Error(err)
} else if string(b) != "value" {
t.Error(err)
}
}

68
main.go Executable file
View File

@ -0,0 +1,68 @@
package main
import (
"local/firestormy/config"
"local/firestormy/server"
"local/lastn/lastn"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"time"
)
func main() {
server := server.New()
if err := server.Routes(); err != nil {
panic(err)
}
go func() {
log.Printf("Serving on %q", config.Port)
if err := http.ListenAndServe(config.Port, server); err != nil {
panic(err)
}
}()
// catch stop
stop := make(chan os.Signal)
signal.Notify(stop, os.Interrupt)
<-stop
}
func EnqueueBackups() {
realpath, err := filepath.Abs(config.StoreAddr)
if err != nil {
log.Println("dir", config.StoreAddr, "not found, so no backups")
return
}
conf := lastn.Config{
N: 3,
Rclone: "/dev/null",
Root: realpath,
Ns: "backups",
Store: "files",
Conf: realpath + "-backups",
}
log.Printf("backups conf: %+v", conf)
lastn, err := lastn.New(conf)
if err != nil {
log.Println("backups disabled:", realpath, ":", err)
return
}
ticker := time.NewTicker(time.Hour * 24)
log.Println("backup initial:", err)
if err := lastn.Push(); err != nil {
log.Println("backup failed:", err)
}
for _ = range ticker.C {
log.Println("backing up...")
if err := lastn.Push(); err != nil {
log.Println("backup push failed:", err)
}
if err := lastn.Clean(); err != nil {
log.Println("backup clean failed:", err)
}
}
}

24
server/.notes/create.go Executable file
View File

@ -0,0 +1,24 @@
package notes
import (
"html"
"local/notes-server/filetree"
"net/http"
"path"
"strings"
)
func (n *Notes) Create(w http.ResponseWriter, r *http.Request) {
content := r.FormValue("base")
content = html.UnescapeString(content)
content = strings.ReplaceAll(content, "\r", "")
urlPath := path.Join(r.URL.Path, content)
p := filetree.NewPathFromURL(urlPath)
if p.IsDir() {
w.WriteHeader(http.StatusBadRequest)
return
}
url := *r.URL
url.Path = path.Join("/edit/", p.BaseHREF)
http.Redirect(w, r, url.String(), http.StatusSeeOther)
}

1
server/.notes/create_test.go Executable file
View File

@ -0,0 +1 @@
package notes

37
server/.notes/dir.go Executable file
View File

@ -0,0 +1,37 @@
package notes
import (
"fmt"
"io/ioutil"
"net/http"
"path"
)
func notesDir(p Path, w http.ResponseWriter, r *http.Request) {
dirs, files := lsDir(p)
content := dirs.List()
notesDirHead(p, w)
block(content, w)
fmt.Fprintln(w, files.List())
}
func notesDirHead(p Path, w http.ResponseWriter) {
fmt.Fprintf(w, `
<form action=%q method="get">
<input type="text" name="base"></input>
<button type="submit">Create</button>
</form>
`, path.Join("/create/", p.BaseHREF))
}
func lsDir(path Path) (Paths, Paths) {
dirs := newDirs()
files := newFiles()
found, _ := ioutil.ReadDir(path.Local)
for _, f := range found {
dirs.Push(path, f)
files.Push(path, f)
}
return Paths(*dirs), Paths(*files)
}

26
server/.notes/dir_test.go Executable file
View File

@ -0,0 +1,26 @@
package notes
import (
"net/http/httptest"
"testing"
)
func TestLsDir(t *testing.T) {
p := Path{Local: "/usr/local"}
dirs, files := lsDir(p)
if len(dirs) == 0 {
t.Fatal(len(dirs))
}
if len(files) == 0 {
t.Fatal(len(files))
}
t.Log(dirs)
t.Log(files)
}
func TestNotesDir(t *testing.T) {
path := Path{Local: "/usr/local"}
w := httptest.NewRecorder()
notesDir(path, w, nil)
t.Logf("%s", w.Body.Bytes())
}

43
server/.notes/edit.go Executable file
View File

@ -0,0 +1,43 @@
package notes
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
)
func (s *Server) edit(w http.ResponseWriter, r *http.Request) {
p := NewPathFromURL(r.URL.Path)
if p.IsDir() {
http.NotFound(w, r)
return
}
head(w, r)
editHead(w, p)
editFile(w, p)
foot(w, r)
}
func editHead(w http.ResponseWriter, p Path) {
fmt.Fprintln(w, h2(p.MultiLink()))
}
func editFile(w http.ResponseWriter, p Path) {
href := p.HREF
href = strings.TrimPrefix(href, "/")
hrefs := strings.SplitN(href, "/", 2)
href = hrefs[0]
if len(hrefs) > 1 {
href = hrefs[1]
}
b, _ := ioutil.ReadFile(p.Local)
fmt.Fprintf(w, `
<form action="/submit/%s" method="post" style="width:100%%; height: 90%%">
<table style="width:100%%; height: 90%%">
<textarea name="content" style="width:100%%; min-height:90%%">%s</textarea>
</table>
<button type="submit">Submit</button>
</form>
`, href, b)
}

1
server/.notes/edit_test.go Executable file
View File

@ -0,0 +1 @@
package notes

29
server/.notes/file.go Executable file
View File

@ -0,0 +1,29 @@
package notes
import (
"fmt"
"io/ioutil"
"net/http"
"path"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
)
func notesFile(p Path, w http.ResponseWriter, r *http.Request) {
b, _ := ioutil.ReadFile(p.Local)
notesFileHead(p, w)
renderer := html.NewRenderer(html.RendererOptions{
Flags: html.CommonFlags | html.TOC,
})
parser := parser.NewWithExtensions(parser.CommonExtensions | parser.HeadingIDs | parser.AutoHeadingIDs | parser.Titleblock)
content := markdown.ToHTML(b, parser, renderer)
fmt.Fprintf(w, "%s\n", content)
}
func notesFileHead(p Path, w http.ResponseWriter) {
fmt.Fprintf(w, `
<a href=%q><input type="button" value="Edit"></input></a>
`, path.Join("/edit/", p.BaseHREF))
}

46
server/.notes/file_test.go Executable file
View File

@ -0,0 +1,46 @@
package notes
import (
"fmt"
"io/ioutil"
"net/http/httptest"
"os"
"strings"
"testing"
)
func TestNotesFile(t *testing.T) {
f, err := ioutil.TempFile(os.TempDir(), "until*")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
fmt.Fprintln(f, `
# Hello
## World
* This
* is
* bullets
| My | table | goes |
|----|-------|------|
| h | e | n |
`)
f.Close()
w := httptest.NewRecorder()
p := Path{Local: f.Name()}
notesFile(p, w, nil)
s := string(w.Body.Bytes())
shouldContain := []string{
"tbody",
"h1",
"h2",
}
for _, should := range shouldContain {
if !strings.Contains(s, should) {
t.Fatalf("%s: %s", should, s)
}
}
t.Logf("%s", s)
}

13
server/.notes/notes.go Executable file
View File

@ -0,0 +1,13 @@
package notes
import "local/notes-server/config"
type Notes struct {
root string
}
func New() *Notes {
return &Notes{
root: config.Root,
}
}

27
server/.notes/rnotes.go Executable file
View File

@ -0,0 +1,27 @@
package notes
import (
"fmt"
"net/http"
)
func (s *Server) notes(w http.ResponseWriter, r *http.Request) {
p := NewPathFromURL(r.URL.Path)
if p.IsDir() {
head(w, r)
notesHead(w, p)
notesDir(p, w, r)
foot(w, r)
} else if p.IsFile() {
head(w, r)
notesHead(w, p)
notesFile(p, w, r)
foot(w, r)
} else {
http.NotFound(w, r)
}
}
func notesHead(w http.ResponseWriter, p Path) {
fmt.Fprintln(w, h2(p.MultiLink()))
}

1
server/.notes/rnotes_test.go Executable file
View File

@ -0,0 +1 @@
package notes

31
server/.notes/submit.go Executable file
View File

@ -0,0 +1,31 @@
package notes
import (
"fmt"
"html"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
)
func (s *Server) submit(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.NotFound(w, r)
return
}
content := r.FormValue("content")
content = html.UnescapeString(content)
content = strings.ReplaceAll(content, "\r", "")
p := NewPathFromURL(r.URL.Path)
os.MkdirAll(path.Dir(p.Local), os.ModePerm)
if err := ioutil.WriteFile(p.Local, []byte(content), os.ModePerm); err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, err)
} else {
url := *r.URL
url.Path = path.Join("/notes/", p.BaseHREF)
http.Redirect(w, r, url.String(), http.StatusSeeOther)
}
}

1
server/.notes/submit_test.go Executable file
View File

@ -0,0 +1 @@
package notes

43
server/routes.go Executable file
View File

@ -0,0 +1,43 @@
package server
import (
"fmt"
"local/gziphttp"
"local/router"
"net/http"
"path/filepath"
)
func (s *Server) Routes() error {
wildcard := router.Wildcard
endpoints := []struct {
path string
handler http.HandlerFunc
}{
{
path: fmt.Sprintf("%s%s", wildcard, wildcard),
handler: s.gzip(s.authenticate(http.NotFound)),
},
}
for _, endpoint := range endpoints {
if err := s.Add(endpoint.path, endpoint.handler); err != nil {
return err
}
}
return nil
}
func (s *Server) gzip(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if gziphttp.Can(r) {
gz := gziphttp.New(w)
defer gz.Close()
w = gz
}
if filepath.Ext(r.URL.Path) == ".css" {
w.Header().Set("Content-Type", "text/css; charset=utf-8")
}
h(w, r)
}
}

32
server/server.go Executable file
View File

@ -0,0 +1,32 @@
package server
import (
"local/firestormy/config"
"local/oauth2/oauth2client"
"local/router"
"log"
"net/http"
)
type Server struct {
*router.Router
}
func New() *Server {
return &Server{
Router: router.New(),
}
}
func (s *Server) authenticate(foo http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if config.OAuthServer != "" {
err := oauth2client.Authenticate(config.OAuthServer, "firestormy", w, r)
if err != nil {
log.Println(err)
return
}
}
foo(w, r)
}
}