491 lines
10 KiB
Go
491 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
// Standard
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"path/filepath"
|
|
"flag"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
"strconv"
|
|
"time"
|
|
_ "embed"
|
|
)
|
|
|
|
const VERSION = "v0.1.2";
|
|
const LISTEN_HOST = "0.0.0.0";
|
|
const DB_SCHEMA = 5
|
|
|
|
var (
|
|
flagPort int
|
|
connectionManager ConnectionManager
|
|
static http.Handler
|
|
config Config
|
|
)
|
|
|
|
|
|
func init() {// {{{
|
|
flag.IntVar(
|
|
&flagPort,
|
|
"port",
|
|
1371,
|
|
"TCP port to listen on",
|
|
)
|
|
|
|
flag.Parse()
|
|
|
|
if false {
|
|
time.Sleep(time.Second*1)
|
|
}
|
|
}// }}}
|
|
func main() {// {{{
|
|
var err error
|
|
log.Printf("\x1b[32mNotes\x1b[0m %s\n", VERSION)
|
|
|
|
config, err = ConfigRead(os.Getenv("HOME")+"/.config/notes.yaml")
|
|
if err != nil {
|
|
fmt.Printf("%s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err = dbInit(); err != nil {
|
|
fmt.Printf("%s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
connectionManager = NewConnectionManager()
|
|
go connectionManager.BroadcastLoop()
|
|
|
|
static = http.FileServer(http.Dir(config.Application.Directories.Static))
|
|
http.HandleFunc("/css_updated", cssUpdateHandler)
|
|
http.HandleFunc("/session/create", sessionCreate)
|
|
http.HandleFunc("/session/retrieve", sessionRetrieve)
|
|
http.HandleFunc("/session/authenticate", sessionAuthenticate)
|
|
http.HandleFunc("/node/tree", nodeTree)
|
|
http.HandleFunc("/node/retrieve", nodeRetrieve)
|
|
http.HandleFunc("/node/create", nodeCreate)
|
|
http.HandleFunc("/node/update", nodeUpdate)
|
|
http.HandleFunc("/node/rename", nodeRename)
|
|
http.HandleFunc("/node/delete", nodeDelete)
|
|
http.HandleFunc("/node/upload", nodeUpload)
|
|
http.HandleFunc("/ws", websocketHandler)
|
|
http.HandleFunc("/", staticHandler)
|
|
|
|
listen := fmt.Sprintf("%s:%d", LISTEN_HOST, flagPort)
|
|
log.Printf("\x1b[32mNotes\x1b[0m Listening on %s\n", listen)
|
|
log.Printf("\x1b[32mNotes\x1b[0m Answer for domains %s\n", strings.Join(config.Websocket.Domains, ", "))
|
|
http.ListenAndServe(listen, nil)
|
|
}// }}}
|
|
|
|
func cssUpdateHandler(w http.ResponseWriter, r *http.Request) {// {{{
|
|
log.Println("[BROADCAST] CSS updated")
|
|
connectionManager.Broadcast(struct{ Ok bool; ID string; Op string }{ Ok: true, Op: "css_reload" })
|
|
}// }}}
|
|
func websocketHandler(w http.ResponseWriter, r *http.Request) {// {{{
|
|
var err error
|
|
|
|
_, err = connectionManager.NewConnection(w, r)
|
|
if err != nil {
|
|
log.Printf("[Connection] %s\n", err)
|
|
return
|
|
}
|
|
}// }}}
|
|
func staticHandler(w http.ResponseWriter, r *http.Request) {// {{{
|
|
data := struct{
|
|
VERSION string
|
|
}{
|
|
VERSION: VERSION,
|
|
}
|
|
|
|
// URLs with pattern /(css|images)/v1.0.0/foobar are stripped of the version.
|
|
// To get rid of problems with cached content in browser on a new version release,
|
|
// while also not disabling cache altogether.
|
|
log.Printf("static: %s", r.URL.Path)
|
|
rxp := regexp.MustCompile("^/(css|images|js|fonts)/v[0-9]+\\.[0-9]+\\.[0-9]+/(.*)$")
|
|
if comp := rxp.FindStringSubmatch(r.URL.Path); comp != nil {
|
|
r.URL.Path = fmt.Sprintf("/%s/%s", comp[1], comp[2])
|
|
static.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Everything else is run through the template system.
|
|
// For now to get VERSION into files to fix caching.
|
|
log.Printf("template: %s", r.URL.Path)
|
|
tmpl, err := newTemplate(r.URL.Path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
w.WriteHeader(404)
|
|
}
|
|
w.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
|
|
if err = tmpl.Execute(w, data); err != nil {
|
|
w.Write([]byte(err.Error()))
|
|
}
|
|
}// }}}
|
|
|
|
func sessionCreate(w http.ResponseWriter, r *http.Request) {// {{{
|
|
log.Println("/session/create")
|
|
session, err := CreateSession()
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"Session": session,
|
|
})
|
|
}// }}}
|
|
func sessionRetrieve(w http.ResponseWriter, r *http.Request) {// {{{
|
|
log.Println("/session/retrieve")
|
|
var err error
|
|
var found bool
|
|
var session Session
|
|
|
|
if session, found, err = ValidateSession(r, false); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"Valid": found,
|
|
"Session": session,
|
|
})
|
|
}// }}}
|
|
func sessionAuthenticate(w http.ResponseWriter, r *http.Request) {// {{{
|
|
log.Println("/session/authenticate")
|
|
var err error
|
|
var session Session
|
|
var authenticated bool
|
|
|
|
// Validate session
|
|
if session, _, err = ValidateSession(r, true); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
req := struct{ Username string; Password string }{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
if authenticated, err = session.Authenticate(req.Username, req.Password); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"Authenticated": authenticated,
|
|
"Session": session,
|
|
})
|
|
}// }}}
|
|
|
|
func nodeTree(w http.ResponseWriter, r *http.Request) {// {{{
|
|
var err error
|
|
var session Session
|
|
|
|
if session, _, err = ValidateSession(r, true); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
req := struct { StartNodeID int }{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
nodes, err := session.NodeTree(req.StartNodeID)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"Nodes": nodes,
|
|
})
|
|
}// }}}
|
|
func nodeRetrieve(w http.ResponseWriter, r *http.Request) {// {{{
|
|
log.Println("/node/retrieve")
|
|
var err error
|
|
var session Session
|
|
|
|
if session, _, err = ValidateSession(r, true); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
req := struct { ID int }{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
node, err := session.Node(req.ID)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"Node": node,
|
|
})
|
|
}// }}}
|
|
func nodeCreate(w http.ResponseWriter, r *http.Request) {// {{{
|
|
log.Println("/node/create")
|
|
var err error
|
|
var session Session
|
|
|
|
if session, _, err = ValidateSession(r, true); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
req := struct {
|
|
Name string
|
|
ParentID int
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
node, err := session.CreateNode(req.ParentID, req.Name)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"Node": node,
|
|
})
|
|
}// }}}
|
|
func nodeUpdate(w http.ResponseWriter, r *http.Request) {// {{{
|
|
var err error
|
|
var session Session
|
|
|
|
if session, _, err = ValidateSession(r, true); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
req := struct {
|
|
NodeID int
|
|
Content string
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
err = session.UpdateNode(req.NodeID, req.Content)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
})
|
|
}// }}}
|
|
func nodeRename(w http.ResponseWriter, r *http.Request) {// {{{
|
|
var err error
|
|
var session Session
|
|
|
|
if session, _, err = ValidateSession(r, true); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
req := struct {
|
|
NodeID int
|
|
Name string
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
err = session.RenameNode(req.NodeID, req.Name)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
})
|
|
}// }}}
|
|
func nodeDelete(w http.ResponseWriter, r *http.Request) {// {{{
|
|
var err error
|
|
var session Session
|
|
|
|
if session, _, err = ValidateSession(r, true); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
req := struct {
|
|
NodeID int
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
err = session.DeleteNode(req.NodeID)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
})
|
|
}// }}}
|
|
func nodeUpload(w http.ResponseWriter, r *http.Request) {// {{{
|
|
log.Println("/node/upload")
|
|
var err error
|
|
var session Session
|
|
|
|
if session, _, err = ValidateSession(r, true); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
// Parse our multipart form, 10 << 20 specifies a maximum
|
|
// upload of 10 MB files.
|
|
r.Body = http.MaxBytesReader(w, r.Body, 128<<20+512)
|
|
r.ParseMultipartForm(128 << 20)
|
|
|
|
// FormFile returns the first file for the given key `myFile`
|
|
// it also returns the FileHeader so we can get the Filename,
|
|
// the Header and the size of the file
|
|
file, handler, err := r.FormFile("file")
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
// Read metadata of file for database, and also file contents
|
|
// for MD5, which is used to store the file on disk.
|
|
fileBytes, err := io.ReadAll(file)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
md5sumBytes := md5.Sum(fileBytes)
|
|
md5sum := hex.EncodeToString(md5sumBytes[:])
|
|
|
|
|
|
var nodeID int
|
|
if nodeID, err = strconv.Atoi(r.PostFormValue("NodeID")); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
nodeFile := File{
|
|
NodeID: nodeID,
|
|
Filename: handler.Filename,
|
|
Size: handler.Size,
|
|
MIME: handler.Header.Get("Content-Type"),
|
|
MD5: md5sum,
|
|
}
|
|
if err = session.AddFile(&nodeFile); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
// Files are stored in a directory structure composed of
|
|
// the first three characters in the md5sum, which is statistically
|
|
// distributed by design, making sure there aren't too many files in
|
|
// a single directory.
|
|
path := filepath.Join(
|
|
config.Application.Directories.Upload,
|
|
md5sum[0:1],
|
|
md5sum[1:2],
|
|
md5sum[2:3],
|
|
)
|
|
if err = os.MkdirAll(path, 0755); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
path = filepath.Join(
|
|
config.Application.Directories.Upload,
|
|
md5sum[0:1],
|
|
md5sum[1:2],
|
|
md5sum[2:3],
|
|
md5sum,
|
|
)
|
|
if err = os.WriteFile(path, fileBytes, 0644); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"File": nodeFile,
|
|
})
|
|
}// }}}
|
|
func nodeFiles(w http.ResponseWriter, r *http.Request) {// {{{
|
|
var err error
|
|
var session Session
|
|
var files []File
|
|
|
|
if session, _, err = ValidateSession(r, true); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
req := struct {
|
|
NodeID int
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
files, err = session.Files(req.NodeID)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"Files": files,
|
|
})
|
|
}// }}}
|
|
|
|
func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{
|
|
// Append index.html if needed for further reading of the file
|
|
p := requestPath
|
|
if p[len(p)-1] == '/' {
|
|
p += "index.html"
|
|
}
|
|
p = config.Application.Directories.Static + p
|
|
|
|
base := path.Base(p)
|
|
if tmpl, err = template.New(base).ParseFiles(p); err != nil { return }
|
|
|
|
return
|
|
}// }}}
|
|
|
|
// vim: foldmethod=marker
|