24 Commits
v1.13 ... v1.21

Author SHA1 Message Date
Bel LaPointe
46e79c0571 do not wrap button with anchor for edit and del 2021-09-03 19:23:45 -06:00
Bel LaPointe
f931053650 search file names first 2021-07-29 07:14:33 -06:00
Bel LaPointe
f2175e41a9 fix tests, 2021-07-29 07:08:49 -06:00
bel
2d16bf0ad5 ensure file size mb at least 1, fix shebang 2021-07-15 19:18:47 -06:00
Bel LaPointe
69ac5f1cbf gitignore vendor 2021-04-08 12:21:04 -05:00
Bel LaPointe
125455fcb6 trim right 2021-04-08 12:10:45 -05:00
Bel LaPointe
1b8f7f86f4 out of bounds 2021-03-29 07:59:29 -05:00
Bel LaPointe
6ea5f2c675 expandtab 2021-03-19 21:18:05 -05:00
Bel LaPointe
3b91c6782d vim editor 2021-03-16 15:55:46 -05:00
Bel LaPointe
5a16beb676 symlinks ok 2021-02-25 22:27:10 -06:00
Bel LaPointe
817fc57dd1 dont search attachments or git 2021-02-25 22:23:13 -06:00
Bel LaPointe
095ba7820d jsut in case 2021-02-25 22:17:32 -06:00
Bel LaPointe
96cce88aed no more todo 2021-02-25 22:10:18 -06:00
Bel LaPointe
baf739658e create pre-commit file to skip big files 2021-02-25 22:09:50 -06:00
Bel LaPointe
0c6d3a6c6a script 2021-02-25 21:53:48 -06:00
Bel LaPointe
e8ea8d8abf try 2021-02-25 20:56:07 -06:00
Bel LaPointe
79009305a1 mm 2021-02-25 20:46:50 -06:00
Bel LaPointe
03f5742a91 grrr 2021-02-25 20:46:19 -06:00
Bel LaPointe
4ad68109d2 gr 2021-02-25 20:34:53 -06:00
Bel LaPointe
61f97f2f0a big 2021-02-25 20:34:08 -06:00
Bel LaPointe
31ebc8ffbc big 2021-02-25 20:33:45 -06:00
Bel LaPointe
567c74bb57 big file 2021-02-25 20:33:30 -06:00
Bel LaPointe
c4161c9db6 big file 2021-02-25 20:32:54 -06:00
Bel LaPointe
fd67e7033b making a big file 2021-02-25 20:19:44 -06:00
20 changed files with 245 additions and 50 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
vendor
gollum gollum
public public
**.sw* **.sw*

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"local/notes-server/config" "local/notes-server/config"
"local/notes-server/notes/editor"
"local/notes-server/server" "local/notes-server/server"
"local/notes-server/versions" "local/notes-server/versions"
"log" "log"
@@ -12,6 +13,7 @@ import (
) )
func main() { func main() {
log.Println(len(editor.CodeMirrorCSS))
server := server.New() server := server.New()
if err := server.Routes(); err != nil { if err := server.Routes(); err != nil {
panic(err) panic(err)

View File

@@ -42,7 +42,7 @@ func TestComment(t *testing.T) {
t.Error(err) 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) t.Error(err)
} }

View File

@@ -1,11 +1,16 @@
package notes package notes
import ( import (
"fmt"
"local/notes-server/filetree" "local/notes-server/filetree"
"os" "os"
) )
func (n *Notes) Delete(urlPath string) error { func (n *Notes) Delete(urlPath string) error {
p := filetree.NewPathFromURL(urlPath) 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
} }

View File

@@ -4,6 +4,7 @@ import (
"io/ioutil" "io/ioutil"
"local/notes-server/config" "local/notes-server/config"
"os" "os"
"path"
"testing" "testing"
) )
@@ -11,17 +12,22 @@ func TestDelete(t *testing.T) {
config.Root = "/tmp" config.Root = "/tmp"
ioutil.WriteFile("/tmp/a", []byte("hi"), os.ModePerm) ioutil.WriteFile("/tmp/a", []byte("hi"), os.ModePerm)
n := &Notes{} n := &Notes{}
t.Run("delete 404", func(t *testing.T) {
if err := n.Delete("/notes/a"); err != nil { if err := n.Delete("/notes/a"); err != nil {
t.Error(err) t.Error(err)
} }
if _, err := os.Stat("/tmp/a"); err == nil { if _, err := os.Stat("/tmp/a"); err == nil {
t.Error(err) t.Error(err)
} }
})
t.Run("delete w. content", func(t *testing.T) {
d, err := ioutil.TempDir(os.TempDir(), "trydel*") d, err := ioutil.TempDir(os.TempDir(), "trydel*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
config.Root = path.Dir(d)
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
f, err := ioutil.TempFile(d, "file*") f, err := ioutil.TempFile(d, "file*")
if err != nil { if err != nil {
@@ -29,15 +35,20 @@ func TestDelete(t *testing.T) {
} }
f.Close() f.Close()
} }
if err := n.Delete(d); err == nil { if err := n.Delete("/abc/" + path.Base(d)); err == nil {
t.Error(err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := n.Delete(e); err != nil { if err := n.Delete("/abc/" + path.Base(d2)); err != nil {
t.Error(err) t.Errorf("failed to del empty dir %s in %s: %v", d2, d2p, err)
} }
})
} }

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"local/notes-server/filetree" "local/notes-server/filetree"
"local/notes-server/notes/editor"
"strings" "strings"
) )
@@ -28,11 +29,47 @@ func editFile(p filetree.Path) string {
return fmt.Sprintf(` return fmt.Sprintf(`
<div class="form"> <div class="form">
<form action="/submit/%s" method="post"> <form action="/submit/%s" method="post">
<table> <textarea id="mytext" name="content" style="cursor:crosshair;">%s</textarea>
<textarea name="content" style="cursor:crosshair;">%s</textarea>
</table>
<button type="submit">Submit</button> <button type="submit">Submit</button>
</form> </form>
</div> </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

File diff suppressed because one or more lines are too long

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
View 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
View 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

File diff suppressed because one or more lines are too long

View File

@@ -68,7 +68,7 @@ func (n *Notes) commentFormer(urlPath string, md []byte) html.RenderNodeFunc {
cur++ cur++
for cur < len(lines) { for cur < len(lines) {
for _, opener_closer := range [][]string{{"```", "```"}, {`<summary>`, `</summary>`}} { 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++ cur++
for cur < len(lines) && !bytes.Contains(lines[cur], []byte(opener_closer[1])) { for cur < len(lines) && !bytes.Contains(lines[cur], []byte(opener_closer[1])) {
cur++ cur++

View File

@@ -3,6 +3,7 @@ package notes
import ( import (
"bufio" "bufio"
"errors" "errors"
"io"
"local/notes-server/filetree" "local/notes-server/filetree"
"log" "log"
"os" "os"
@@ -51,42 +52,59 @@ func (n *Notes) Search(phrase string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
perffiles := filetree.NewFiles()
files := filetree.NewFiles() files := filetree.NewFiles()
err = filepath.Walk(n.root, err = filepath.Walk(n.root,
func(walked string, info os.FileInfo, err error) error { func(walked string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err 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 return nil
} }
if size := info.Size(); size < 1 || size > (5*1024*1024) { if size := info.Size(); size < 1 || size > (5*1024*1024) {
return nil return nil
} }
ok, err := grepFile(walked, searcher) ok, err := searcher.file(walked)
if err != nil && err.Error() == "bufio.Scanner: token too long" { if err != nil && err.Error() == "bufio.Scanner: token too long" {
err = nil err = nil
} }
if err == nil && ok {
p := filetree.NewPathFromLocal(path.Dir(walked))
files.Push(p, info)
}
if err != nil { if err != nil {
log.Printf("failed to scan %v: %v", walked, err) log.Printf("failed to scan %v: %v", walked, err)
} else if ok {
files.Push(p, info)
} }
return err 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
}
if strings.Contains(file, "/.git/") {
return false, nil
}
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
return false, err return false, err
} }
defer f.Close() 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() { for scanner.Scan() {
if searcher.matches(scanner.Bytes()) { if searcher.matches(scanner.Bytes()) {
return true, scanner.Err() return true, scanner.Err()

View File

@@ -34,7 +34,7 @@ func TestSearch(t *testing.T) {
t.Fatal(v, result) t.Fatal(v, result)
} }
result, err = n.Search("4") result, err = n.Search("number.4")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -66,8 +66,8 @@ func htmlEdit(w http.ResponseWriter, baseHREF string) {
if config.ReadOnly { if config.ReadOnly {
return return
} }
fmt.Fprintf(w, `<div style='display:inline-block'> fmt.Fprintf(w, `<div style='display:inline-block; margin-top: .75em; margin-bottom: .75em;'>
<a href=%q><input type="button" value="Edit"></input></a> <a href=%q class="button">Edit</a>
</div><br>`, path.Join("/edit/", baseHREF)) </div><br>`, path.Join("/edit/", baseHREF))
} }
@@ -75,8 +75,8 @@ func htmlDelete(w http.ResponseWriter, baseHREF string) {
if config.ReadOnly { if config.ReadOnly {
return return
} }
fmt.Fprintf(w, `<div style='display:inline-block'> fmt.Fprintf(w, `<div style='display:inline-block; margin-top: .75em; margin-bottom: .75em;'>
<a href=%q><input type="button" value="Delete" onclick="return confirm('Delete?');"></input></a> <a href=%q class="button" onclick="return confirm('Delete?');">Delete</a>
</div><br>`, path.Join("/delete/", baseHREF)) </div><br>`, path.Join("/delete/", baseHREF))
} }
@@ -84,7 +84,6 @@ func (s *Server) htmlAttachments(w http.ResponseWriter, urlPath string) {
dir := path.Dir(urlPath) dir := path.Dir(urlPath)
f := "." + path.Base(urlPath) + ".attachments" f := "." + path.Base(urlPath) + ".attachments"
_, files, _ := s.Notes.Dir(path.Join(dir, f)) _, files, _ := s.Notes.Dir(path.Join(dir, f))
// TODO replace <a with <a download UNLESS img, then... hrm
form := fmt.Sprintf(` form := fmt.Sprintf(`
<form enctype="multipart/form-data" action="/attach/%s" method="post"> <form enctype="multipart/form-data" action="/attach/%s" method="post">
<input type="file" name="file" required/> <input type="file" name="file" required/>

View File

@@ -1,6 +1,8 @@
package server package server
import ( import (
"bytes"
"fmt"
"html" "html"
"local/notes-server/filetree" "local/notes-server/filetree"
"net/http" "net/http"
@@ -16,6 +18,7 @@ func (s *Server) submit(w http.ResponseWriter, r *http.Request) {
content := r.FormValue("content") content := r.FormValue("content")
content = html.UnescapeString(content) content = html.UnescapeString(content)
content = strings.ReplaceAll(content, "\r", "") content = strings.ReplaceAll(content, "\r", "")
content = trimLines(content)
err := s.Notes.Submit(r.URL.Path, content) err := s.Notes.Submit(r.URL.Path, content)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) 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) url.Path = path.Join("/notes/", filetree.NewPathFromURL(r.URL.Path).BaseHREF)
http.Redirect(w, r, url.String(), http.StatusSeeOther) 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
View 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)
}
})
}

38
versions/max_file_size.go Normal file
View File

@@ -0,0 +1,38 @@
package versions
import (
"fmt"
"local/notes-server/config"
"strings"
)
func getScript() string {
maxSizeMB := config.MaxSizeMB
if maxSizeMB == 0 {
maxSizeMB = 1
}
return strings.ReplaceAll(script, "{{{MAXSIZE}}}", fmt.Sprint(maxSizeMB<<20))
}
const script = `#!/bin/bash
function main() {
local maxsize={{{MAXSIZE}}}
if [[ "$maxsize" == 0 ]]; then
return
fi
(
git diff --name-only --cached
git diff --name-only
git ls-files --others --exclude-standard
) 2>&1 \
| sort -u \
| while read -r file; do
local size="$(du -sk "$file" | awk '{print $1}')000"
if [ "$size" -gt "$maxsize" ]; then
echo "file=$file, size=$size, max=$maxsize" >&2
git reset HEAD -- "$file"
fi
done
}
main
`

View File

@@ -2,8 +2,13 @@ package versions
import ( import (
"fmt" "fmt"
"io/ioutil"
"local/notes-server/config" "local/notes-server/config"
"log"
"os"
"os/exec" "os/exec"
"path"
"strings"
"time" "time"
) )
@@ -15,7 +20,9 @@ func New() (*Versions, error) {
v.cmd("git", "init") v.cmd("git", "init")
v.cmd("git", "config", "user.email", "user@user.user") v.cmd("git", "config", "user.email", "user@user.user")
v.cmd("git", "config", "user.name", "user") v.cmd("git", "config", "user.name", "user")
return v, nil s := getScript()
err := ioutil.WriteFile(path.Join(config.Root, "./.git/hooks/pre-commit"), []byte(s), os.ModePerm)
return v, err
} }
func (v *Versions) Gitmmit() error { func (v *Versions) Gitmmit() error {
@@ -39,6 +46,9 @@ func (v *Versions) Commit() error {
func (v *Versions) cmd(cmd string, args ...string) error { func (v *Versions) cmd(cmd string, args ...string) error {
command := exec.Command(cmd, args...) command := exec.Command(cmd, args...)
command.Dir = config.Root command.Dir = config.Root
_, err := command.CombinedOutput() out, err := command.CombinedOutput()
if err != nil {
log.Println(cmd, args, ":", strings.TrimSpace(string(out)))
}
return err return err
} }