Base project to template
commit
f022b26987
|
|
@ -0,0 +1,10 @@
|
||||||
|
gollum
|
||||||
|
public
|
||||||
|
**.sw*
|
||||||
|
**/**.sw*
|
||||||
|
*.sw*
|
||||||
|
**/*.sw*
|
||||||
|
notes-server
|
||||||
|
exec-notes-server
|
||||||
|
firestormy
|
||||||
|
exec-firestormy
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
package notes
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
package notes
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package notes
|
||||||
|
|
||||||
|
import "local/notes-server/config"
|
||||||
|
|
||||||
|
type Notes struct {
|
||||||
|
root string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Notes {
|
||||||
|
return &Notes{
|
||||||
|
root: config.Root,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
package notes
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
package notes
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue