Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46e79c0571 | ||
|
|
f931053650 | ||
|
|
f2175e41a9 | ||
|
|
2d16bf0ad5 | ||
|
|
69ac5f1cbf | ||
|
|
125455fcb6 | ||
|
|
1b8f7f86f4 | ||
|
|
6ea5f2c675 | ||
|
|
3b91c6782d | ||
|
|
5a16beb676 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
|
||||
vendor
|
||||
gollum
|
||||
public
|
||||
**.sw*
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
main.go
2
main.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"local/notes-server/config"
|
||||
"local/notes-server/notes/editor"
|
||||
"local/notes-server/server"
|
||||
"local/notes-server/versions"
|
||||
"log"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println(len(editor.CodeMirrorCSS))
|
||||
server := server.New()
|
||||
if err := server.Routes(); err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestComment(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := n.Comment(fpath, 10000, "big line no"); err == nil {
|
||||
if err := n.Comment(fpath, 10000, "big line no"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package notes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"local/notes-server/filetree"
|
||||
"os"
|
||||
)
|
||||
|
||||
func (n *Notes) Delete(urlPath string) error {
|
||||
p := filetree.NewPathFromURL(urlPath)
|
||||
return os.Remove(p.Local)
|
||||
err := os.Remove(p.Local)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to delete %q => %q: %v", urlPath, p.Local, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"io/ioutil"
|
||||
"local/notes-server/config"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -11,17 +12,22 @@ func TestDelete(t *testing.T) {
|
||||
config.Root = "/tmp"
|
||||
ioutil.WriteFile("/tmp/a", []byte("hi"), os.ModePerm)
|
||||
n := &Notes{}
|
||||
|
||||
t.Run("delete 404", func(t *testing.T) {
|
||||
if err := n.Delete("/notes/a"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := os.Stat("/tmp/a"); err == nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete w. content", func(t *testing.T) {
|
||||
d, err := ioutil.TempDir(os.TempDir(), "trydel*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config.Root = path.Dir(d)
|
||||
for i := 0; i < 3; i++ {
|
||||
f, err := ioutil.TempFile(d, "file*")
|
||||
if err != nil {
|
||||
@@ -29,15 +35,20 @@ func TestDelete(t *testing.T) {
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
if err := n.Delete(d); err == nil {
|
||||
if err := n.Delete("/abc/" + path.Base(d)); err == nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
e, err := ioutil.TempDir(os.TempDir(), "trydel*")
|
||||
t.Run("delete empty dir", func(t *testing.T) {
|
||||
d2p := os.TempDir()
|
||||
d2, err := ioutil.TempDir(d2p, "trydel*")
|
||||
config.Root = path.Dir(d2p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := n.Delete(e); err != nil {
|
||||
t.Error(err)
|
||||
if err := n.Delete("/abc/" + path.Base(d2)); err != nil {
|
||||
t.Errorf("failed to del empty dir %s in %s: %v", d2, d2p, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"local/notes-server/filetree"
|
||||
"local/notes-server/notes/editor"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -28,11 +29,47 @@ func editFile(p filetree.Path) string {
|
||||
return fmt.Sprintf(`
|
||||
<div class="form">
|
||||
<form action="/submit/%s" method="post">
|
||||
<table>
|
||||
<textarea name="content" style="cursor:crosshair;">%s</textarea>
|
||||
</table>
|
||||
<textarea id="mytext" name="content" style="cursor:crosshair;">%s</textarea>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
`, href, b)
|
||||
<!--
|
||||
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/codemirror.min.js
|
||||
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/codemirror.min.css
|
||||
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/keymap/vim.min.js
|
||||
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.59.4/theme/dracula.min.css
|
||||
-->
|
||||
<script>%s</script>
|
||||
<style>%s</style>
|
||||
<script>%s</script>
|
||||
<style>%s</style>
|
||||
<script>
|
||||
if( ! /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
|
||||
CodeMirror.fromTextArea(document.getElementById("mytext"), {
|
||||
lineNumbers: true,
|
||||
mode: "md",
|
||||
theme: "dracula",
|
||||
keyMap: "vim",
|
||||
smartIndent: true,
|
||||
indentUnit: 3,
|
||||
tabSize: 3,
|
||||
indentWithTabs: false,
|
||||
lineWrapping: true,
|
||||
autofocus: true,
|
||||
dragDrop: false,
|
||||
spellcheck: true,
|
||||
autocorrect: false,
|
||||
autocapitalize: false,
|
||||
extraKeys: {
|
||||
Tab: (cm) => {
|
||||
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ");
|
||||
cm.replaceSelection(spaces);
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
console.log(navigator.userAgent)
|
||||
}
|
||||
</script>
|
||||
`, href, b, editor.CodeMirrorJS, editor.CodeMirrorCSS, editor.CodeMirrorVIM, editor.CodeMirrorTheme)
|
||||
}
|
||||
|
||||
1
notes/editor/codemirror.min.css
vendored
Normal file
1
notes/editor/codemirror.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
notes/editor/codemirror.min.js
vendored
Normal file
1
notes/editor/codemirror.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
notes/editor/dracula.min.css
vendored
Normal file
1
notes/editor/dracula.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.cm-s-dracula .CodeMirror-gutters,.cm-s-dracula.CodeMirror{background-color:#282a36!important;color:#f8f8f2!important;border:none}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:solid thin #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:rgba(255,255,255,.1)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:rgba(255,255,255,.1)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:rgba(255,255,255,.1)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:#fff}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-keyword{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute{color:#50fa7b}.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-type,.cm-s-dracula span.cm-variable-3{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:rgba(255,255,255,.1)}.cm-s-dracula .CodeMirror-matchingbracket{text-decoration:underline;color:#fff!important}
|
||||
17
notes/editor/embed.go
Normal file
17
notes/editor/embed.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package editor
|
||||
|
||||
import _ "embed"
|
||||
|
||||
var (
|
||||
//go:embed codemirror.min.js
|
||||
CodeMirrorJS string
|
||||
|
||||
//go:embed codemirror.min.css
|
||||
CodeMirrorCSS string
|
||||
|
||||
//go:embed vim.min.js
|
||||
CodeMirrorVIM string
|
||||
|
||||
//go:embed dracula.min.css
|
||||
CodeMirrorTheme string
|
||||
)
|
||||
1
notes/editor/vim.min.js
vendored
Normal file
1
notes/editor/vim.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -68,7 +68,7 @@ func (n *Notes) commentFormer(urlPath string, md []byte) html.RenderNodeFunc {
|
||||
cur++
|
||||
for cur < len(lines) {
|
||||
for _, opener_closer := range [][]string{{"```", "```"}, {`<summary>`, `</summary>`}} {
|
||||
if bytes.Contains(lines[cur], []byte(opener_closer[0])) {
|
||||
if cur < len(lines) && bytes.Contains(lines[cur], []byte(opener_closer[0])) {
|
||||
cur++
|
||||
for cur < len(lines) && !bytes.Contains(lines[cur], []byte(opener_closer[1])) {
|
||||
cur++
|
||||
|
||||
@@ -3,6 +3,7 @@ package notes
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"local/notes-server/filetree"
|
||||
"log"
|
||||
"os"
|
||||
@@ -51,36 +52,43 @@ func (n *Notes) Search(phrase string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
perffiles := filetree.NewFiles()
|
||||
files := filetree.NewFiles()
|
||||
err = filepath.Walk(n.root,
|
||||
func(walked string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
p := filetree.NewPathFromLocal(path.Dir(walked))
|
||||
if ok, _ := searcher.stream(strings.NewReader(info.Name())); ok {
|
||||
perffiles.Push(p, info)
|
||||
return nil
|
||||
}
|
||||
if size := info.Size(); size < 1 || size > (5*1024*1024) {
|
||||
return nil
|
||||
}
|
||||
ok, err := grepFile(walked, searcher)
|
||||
ok, err := searcher.file(walked)
|
||||
if err != nil && err.Error() == "bufio.Scanner: token too long" {
|
||||
err = nil
|
||||
}
|
||||
if err == nil && ok {
|
||||
p := filetree.NewPathFromLocal(path.Dir(walked))
|
||||
files.Push(p, info)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("failed to scan %v: %v", walked, err)
|
||||
} else if ok {
|
||||
files.Push(p, info)
|
||||
}
|
||||
return err
|
||||
},
|
||||
)
|
||||
return filetree.Paths(*files).List(true), err
|
||||
for _, file := range *files {
|
||||
*perffiles = append(*perffiles, file)
|
||||
}
|
||||
return filetree.Paths(*perffiles).List(true), err
|
||||
}
|
||||
|
||||
func grepFile(file string, searcher *searcher) (bool, error) {
|
||||
func (searcher *searcher) file(file string) (bool, error) {
|
||||
if d := path.Base(path.Dir(file)); strings.HasPrefix(d, ".") && strings.HasSuffix(d, ".attachments") {
|
||||
return false, nil
|
||||
}
|
||||
@@ -92,7 +100,11 @@ func grepFile(file string, searcher *searcher) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
defer f.Close()
|
||||
scanner := bufio.NewScanner(f)
|
||||
return searcher.stream(f)
|
||||
}
|
||||
|
||||
func (searcher *searcher) stream(r io.Reader) (bool, error) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
if searcher.matches(scanner.Bytes()) {
|
||||
return true, scanner.Err()
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestSearch(t *testing.T) {
|
||||
t.Fatal(v, result)
|
||||
}
|
||||
|
||||
result, err = n.Search("4")
|
||||
result, err = n.Search("number.4")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -66,8 +66,8 @@ func htmlEdit(w http.ResponseWriter, baseHREF string) {
|
||||
if config.ReadOnly {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, `<div style='display:inline-block'>
|
||||
<a href=%q><input type="button" value="Edit"></input></a>
|
||||
fmt.Fprintf(w, `<div style='display:inline-block; margin-top: .75em; margin-bottom: .75em;'>
|
||||
<a href=%q class="button">Edit</a>
|
||||
</div><br>`, path.Join("/edit/", baseHREF))
|
||||
}
|
||||
|
||||
@@ -75,8 +75,8 @@ func htmlDelete(w http.ResponseWriter, baseHREF string) {
|
||||
if config.ReadOnly {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, `<div style='display:inline-block'>
|
||||
<a href=%q><input type="button" value="Delete" onclick="return confirm('Delete?');"></input></a>
|
||||
fmt.Fprintf(w, `<div style='display:inline-block; margin-top: .75em; margin-bottom: .75em;'>
|
||||
<a href=%q class="button" onclick="return confirm('Delete?');">Delete</a>
|
||||
</div><br>`, path.Join("/delete/", baseHREF))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"local/notes-server/filetree"
|
||||
"net/http"
|
||||
@@ -16,6 +18,7 @@ func (s *Server) submit(w http.ResponseWriter, r *http.Request) {
|
||||
content := r.FormValue("content")
|
||||
content = html.UnescapeString(content)
|
||||
content = strings.ReplaceAll(content, "\r", "")
|
||||
content = trimLines(content)
|
||||
err := s.Notes.Submit(r.URL.Path, content)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@@ -25,3 +28,12 @@ func (s *Server) submit(w http.ResponseWriter, r *http.Request) {
|
||||
url.Path = path.Join("/notes/", filetree.NewPathFromURL(r.URL.Path).BaseHREF)
|
||||
http.Redirect(w, r, url.String(), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func trimLines(s string) string {
|
||||
buff := bytes.NewBuffer(nil)
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
fmt.Fprintln(buff, strings.TrimRight(line, "\n \r\t"))
|
||||
}
|
||||
fixed := string(buff.Bytes())
|
||||
return fixed[:len(fixed)-1]
|
||||
}
|
||||
|
||||
36
server/submit_test.go
Normal file
36
server/submit_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package server
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTrimLines(t *testing.T) {
|
||||
t.Run("no newline at end", func(t *testing.T) {
|
||||
s := `
|
||||
hello
|
||||
world`
|
||||
s += " "
|
||||
s += " \t"
|
||||
got := trimLines(s)
|
||||
want := `
|
||||
hello
|
||||
world`
|
||||
if got != want {
|
||||
t.Fatalf("want %q, got %q", want, got)
|
||||
}
|
||||
})
|
||||
t.Run("noop", func(t *testing.T) {
|
||||
s := `hi`
|
||||
got := trimLines(s)
|
||||
want := `hi`
|
||||
if got != want {
|
||||
t.Fatalf("want %q, got %q", want, got)
|
||||
}
|
||||
})
|
||||
t.Run("newline", func(t *testing.T) {
|
||||
s := "hi\n"
|
||||
got := trimLines(s)
|
||||
want := "hi\n"
|
||||
if got != want {
|
||||
t.Fatalf("want %q, got %q", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -7,11 +7,14 @@ import (
|
||||
)
|
||||
|
||||
func getScript() string {
|
||||
return strings.ReplaceAll(script, "{{{MAXSIZE}}}", fmt.Sprint(config.MaxSizeMB<<20))
|
||||
maxSizeMB := config.MaxSizeMB
|
||||
if maxSizeMB == 0 {
|
||||
maxSizeMB = 1
|
||||
}
|
||||
return strings.ReplaceAll(script, "{{{MAXSIZE}}}", fmt.Sprint(maxSizeMB<<20))
|
||||
}
|
||||
|
||||
const script = `
|
||||
#!/bin/bash
|
||||
const script = `#!/bin/bash
|
||||
function main() {
|
||||
local maxsize={{{MAXSIZE}}}
|
||||
if [[ "$maxsize" == 0 ]]; then
|
||||
|
||||
Reference in New Issue
Block a user