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