562 lines
12 KiB
Go
562 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
// External
|
|
"git.gibonuddevalla.se/go/webservice"
|
|
|
|
// Internal
|
|
"git.gibonuddevalla.se/go/webservice/session"
|
|
|
|
// Standard
|
|
"crypto/md5"
|
|
"embed"
|
|
"encoding/hex"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const LISTEN_HOST = "0.0.0.0"
|
|
|
|
var (
|
|
flagPort int
|
|
flagVersion bool
|
|
flagCreateUser bool
|
|
flagCheckLocal bool
|
|
flagConfig string
|
|
|
|
service *webservice.Service
|
|
connectionManager ConnectionManager
|
|
static http.Handler
|
|
config Config
|
|
logger *slog.Logger
|
|
VERSION string
|
|
|
|
//go:embed version sql/*
|
|
embeddedSQL embed.FS
|
|
|
|
//go:embed static
|
|
staticFS embed.FS
|
|
)
|
|
|
|
func sqlProvider(dbname string, version int) (sql []byte, found bool) {
|
|
var err error
|
|
sql, err = embeddedSQL.ReadFile(fmt.Sprintf("sql/%05d.sql", version))
|
|
if err != nil {
|
|
return
|
|
}
|
|
found = true
|
|
return
|
|
}
|
|
|
|
func init() { // {{{
|
|
version, _ := embeddedSQL.ReadFile("version")
|
|
VERSION = strings.TrimSpace(string(version))
|
|
|
|
opt := slog.HandlerOptions{}
|
|
opt.Level = slog.LevelDebug
|
|
logger = slog.New(slog.NewJSONHandler(os.Stdout, &opt))
|
|
|
|
configFilename := os.Getenv("HOME") + "/.config/notes.yaml"
|
|
flag.IntVar(&flagPort, "port", 1371, "TCP port to listen on")
|
|
flag.BoolVar(&flagVersion, "version", false, "Shows Notes version and exists")
|
|
flag.BoolVar(&flagCreateUser, "createuser", false, "Create a user and exit")
|
|
flag.BoolVar(&flagCheckLocal, "checklocal", false, "Check for local static file before embedded")
|
|
flag.StringVar(&flagConfig, "config", configFilename, "Filename of configuration file")
|
|
flag.Parse()
|
|
} // }}}
|
|
func main() { // {{{
|
|
var err error
|
|
|
|
if flagVersion {
|
|
fmt.Printf("%s\n", VERSION)
|
|
os.Exit(0)
|
|
}
|
|
logger.Info("application", "version", VERSION)
|
|
|
|
config, err = ConfigRead(flagConfig)
|
|
if err != nil {
|
|
logger.Error("application", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
service, err = webservice.New(flagConfig, VERSION, logger)
|
|
if err != nil {
|
|
logger.Error("application", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
service.SetDatabase(sqlProvider)
|
|
service.SetStaticDirectory("static", true)
|
|
service.SetStaticFS(staticFS, "static")
|
|
|
|
service.Register("/node/upload", true, true, nodeUpload)
|
|
service.Register("/node/tree", true, true, nodeTree)
|
|
service.Register("/node/retrieve", true, true, nodeRetrieve)
|
|
service.Register("/node/create", true, true, nodeCreate)
|
|
service.Register("/node/update", true, true, nodeUpdate)
|
|
service.Register("/node/rename", true, true, nodeRename)
|
|
service.Register("/node/delete", true, true, nodeDelete)
|
|
service.Register("/node/download", true, true, nodeDownload)
|
|
service.Register("/node/search", true, true, nodeSearch)
|
|
service.Register("/node/checklist_item/state", true, true, nodeChecklistItemState)
|
|
service.Register("/key/retrieve", true, true, keyRetrieve)
|
|
service.Register("/key/create", true, true, keyCreate)
|
|
service.Register("/key/counter", true, true, keyCounter)
|
|
service.Register("/", false, false, service.StaticHandler)
|
|
|
|
if flagCreateUser {
|
|
service.CreateUserPrompt()
|
|
os.Exit(0)
|
|
}
|
|
|
|
err = service.Start()
|
|
if err != nil {
|
|
logger.Error("webserver", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
} // }}}
|
|
|
|
/*
|
|
func userPassword(w http.ResponseWriter, r *http.Request) { // {{{
|
|
var err error
|
|
var ok bool
|
|
var session Session
|
|
|
|
if session, _, err = ValidateSession(r, true); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
req := struct {
|
|
CurrentPassword string
|
|
NewPassword string
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
ok, err = session.UpdatePassword(req.CurrentPassword, req.NewPassword)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"CurrentPasswordOK": ok,
|
|
})
|
|
} // }}}
|
|
*/
|
|
|
|
func nodeTree(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
logger.Info("webserver", "request", "/node/tree")
|
|
var err error
|
|
|
|
req := struct{ StartNodeID int }{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
nodes, err := NodeTree(sess.UserID, 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, sess *session.T) { // {{{
|
|
logger.Info("webserver", "request", "/node/retrieve")
|
|
var err error
|
|
|
|
req := struct{ ID int }{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
node, err := RetrieveNode(sess.UserID, 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, sess *session.T) { // {{{
|
|
logger.Info("webserver", "request", "/node/create")
|
|
var err error
|
|
|
|
req := struct {
|
|
Name string
|
|
ParentID int
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
node, err := CreateNode(sess.UserID, 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, sess *session.T) { // {{{
|
|
logger.Info("webserver", "request", "/node/update")
|
|
var err error
|
|
|
|
req := struct {
|
|
NodeID int
|
|
Content string
|
|
CryptoKeyID int
|
|
Markdown bool
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
err = UpdateNode(sess.UserID, req.NodeID, req.Content, req.CryptoKeyID, req.Markdown)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
})
|
|
} // }}}
|
|
func nodeRename(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
var err error
|
|
logger.Info("webserver", "request", "/node/rename")
|
|
|
|
req := struct {
|
|
NodeID int
|
|
Name string
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
err = RenameNode(sess.UserID, 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, sess *session.T) { // {{{
|
|
var err error
|
|
logger.Info("webserver", "request", "/node/delete")
|
|
|
|
req := struct {
|
|
NodeID int
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
err = DeleteNode(sess.UserID, req.NodeID)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
})
|
|
} // }}}
|
|
func nodeUpload(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
logger.Info("webserver", "request", "/node/upload")
|
|
var err error
|
|
|
|
// 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 = AddFile(sess.UserID, &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 nodeDownload(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
logger.Info("webserver", "request", "/node/download")
|
|
var err error
|
|
var files []File
|
|
|
|
req := struct {
|
|
NodeID int
|
|
FileID int
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
files, err = Files(sess.UserID, req.NodeID, req.FileID)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
if len(files) != 1 {
|
|
responseError(w, fmt.Errorf("File not found"))
|
|
return
|
|
}
|
|
|
|
var file *os.File
|
|
fname := filepath.Join(
|
|
config.Application.Directories.Upload,
|
|
files[0].MD5[0:1],
|
|
files[0].MD5[1:2],
|
|
files[0].MD5[2:3],
|
|
files[0].MD5,
|
|
)
|
|
file, err = os.Open(fname)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
var finfo os.FileInfo
|
|
finfo, err = file.Stat()
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Content-Type", files[0].MIME)
|
|
w.Header().Add("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, files[0].Filename))
|
|
w.Header().Add("Content-Length", strconv.Itoa(int(finfo.Size())))
|
|
|
|
read := 1
|
|
var buf []byte
|
|
for read > 0 {
|
|
buf = make([]byte, 65536)
|
|
read, err = file.Read(buf)
|
|
if read > 0 {
|
|
w.Write(buf[0:read])
|
|
}
|
|
}
|
|
|
|
} // }}}
|
|
func nodeFiles(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
logger.Info("webserver", "request", "/node/files")
|
|
var err error
|
|
var files []File
|
|
|
|
req := struct {
|
|
NodeID int
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
files, err = Files(sess.UserID, req.NodeID, 0)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"Files": files,
|
|
})
|
|
} // }}}
|
|
func nodeSearch(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
logger.Info("webserver", "request", "/node/search")
|
|
var err error
|
|
var nodes []Node
|
|
|
|
req := struct {
|
|
Search string
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
nodes, err = SearchNodes(sess.UserID, req.Search)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"Nodes": nodes,
|
|
})
|
|
} // }}}
|
|
func nodeChecklistItemState(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
logger.Info("webserver", "request", "/node/checklist_item/state")
|
|
var err error
|
|
|
|
req := struct {
|
|
ChecklistItemID int
|
|
State bool
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
err = ChecklistItemState(sess.UserID, req.ChecklistItemID, req.State)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
})
|
|
} // }}}
|
|
|
|
func keyRetrieve(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
logger.Info("webserver", "request", "/key/retrieve")
|
|
var err error
|
|
|
|
keys, err := Keys(sess.UserID)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"Keys": keys,
|
|
})
|
|
} // }}}
|
|
func keyCreate(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
logger.Info("webserver", "request", "/key/create")
|
|
var err error
|
|
|
|
req := struct {
|
|
Description string
|
|
Key string
|
|
}{}
|
|
if err = parseRequest(r, &req); err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
key, err := KeyCreate(sess.UserID, req.Description, req.Key)
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
"Key": key,
|
|
})
|
|
} // }}}
|
|
func keyCounter(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
|
logger.Info("webserver", "request", "/key/counter")
|
|
var err error
|
|
|
|
counter, err := KeyCounter()
|
|
if err != nil {
|
|
responseError(w, err)
|
|
return
|
|
}
|
|
|
|
responseData(w, map[string]interface{}{
|
|
"OK": true,
|
|
// Javascript uses int32, thus getting a string counter for Javascript BigInt to parse.
|
|
"Counter": strconv.FormatInt(counter, 10),
|
|
})
|
|
} // }}}
|
|
|
|
// vim: foldmethod=marker
|