Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f931053650 | ||
|
|
f2175e41a9 | ||
|
|
2d16bf0ad5 | ||
|
|
69ac5f1cbf | ||
|
|
125455fcb6 | ||
|
|
1b8f7f86f4 | ||
|
|
6ea5f2c675 | ||
|
|
3b91c6782d | ||
|
|
5a16beb676 | ||
|
|
817fc57dd1 | ||
|
|
095ba7820d | ||
|
|
96cce88aed | ||
|
|
baf739658e | ||
|
|
0c6d3a6c6a | ||
|
|
e8ea8d8abf | ||
|
|
79009305a1 | ||
|
|
03f5742a91 | ||
|
|
4ad68109d2 | ||
|
|
61f97f2f0a | ||
|
|
31ebc8ffbc | ||
|
|
567c74bb57 | ||
|
|
c4161c9db6 | ||
|
|
fd67e7033b | ||
|
|
cbd287e20e | ||
|
|
479caef353 | ||
|
|
8fefc60f6b | ||
|
|
014076dd06 | ||
|
|
d408229ba9 | ||
|
|
47d6c37819 | ||
|
|
1eb8fa7223 | ||
|
|
f9c28b3c45 | ||
|
|
a26d34c2b3 | ||
|
|
4d076ede3d | ||
|
|
f60d0618e6 | ||
|
|
b975a7c103 | ||
|
|
0e9150b8f6 | ||
|
|
4fdbee6df5 | ||
|
|
e6a126ea0b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
|
||||||
|
vendor
|
||||||
gollum
|
gollum
|
||||||
public
|
public
|
||||||
**.sw*
|
**.sw*
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ var (
|
|||||||
OAuthServer string
|
OAuthServer string
|
||||||
VersionInterval time.Duration
|
VersionInterval time.Duration
|
||||||
ReadOnly bool
|
ReadOnly bool
|
||||||
|
MaxSizeMB int
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -38,6 +39,7 @@ func Refresh() {
|
|||||||
as.Append(args.STRING, "oauth", "oauth URL", "")
|
as.Append(args.STRING, "oauth", "oauth URL", "")
|
||||||
as.Append(args.DURATION, "version", "duration to mark versions", "0s")
|
as.Append(args.DURATION, "version", "duration to mark versions", "0s")
|
||||||
as.Append(args.BOOL, "ro", "read-only mode", false)
|
as.Append(args.BOOL, "ro", "read-only mode", false)
|
||||||
|
as.Append(args.INT, "max-v-size", "max size in mb for versioning", 2)
|
||||||
if err := as.Parse(); err != nil {
|
if err := as.Parse(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -67,6 +69,7 @@ func Refresh() {
|
|||||||
OAuthServer = as.GetString("oauth")
|
OAuthServer = as.GetString("oauth")
|
||||||
VersionInterval = as.GetDuration("version")
|
VersionInterval = as.GetDuration("version")
|
||||||
ReadOnly = as.GetBool("ro")
|
ReadOnly = as.GetBool("ro")
|
||||||
|
MaxSizeMB = as.GetInt("max-v-size")
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultWrapper = `
|
const defaultWrapper = `
|
||||||
@@ -102,9 +105,23 @@ const defaultWrapper = `
|
|||||||
font-size: 125%;
|
font-size: 125%;
|
||||||
background: #3b242b;
|
background: #3b242b;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
height: calc(100vh - 1em);
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
body > form > textarea {
|
body > .form {
|
||||||
margin-top: 2.5em;
|
flex-grow: 2;
|
||||||
|
}
|
||||||
|
body > .form > form {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
body > .form > form > textarea ,
|
||||||
|
body > .form > form > div {
|
||||||
|
flex-grow: 2;
|
||||||
}
|
}
|
||||||
rendered > h2:nth-child(2) {
|
rendered > h2:nth-child(2) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -135,10 +152,11 @@ const defaultWrapper = `
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
margin-bottom: .5em;
|
margin-bottom: .5em;
|
||||||
padding: .3em 0 .3em 0;
|
padding: .3em 0 .3em 0;
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background-color: #3b242b;
|
background-color: #3b242b;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
position: -webkit-sticky;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
h1:hover > .comment,
|
h1:hover > .comment,
|
||||||
h2:hover > .comment,
|
h2:hover > .comment,
|
||||||
@@ -152,9 +170,17 @@ const defaultWrapper = `
|
|||||||
.comment:focus-within {
|
.comment:focus-within {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
.attachments > details > form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.attachments > details > form > input:first-of-type {
|
||||||
|
flex-grow: 2;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</header>
|
</header>
|
||||||
<body height="100%">
|
<body>
|
||||||
{{{}}}
|
{{{}}}
|
||||||
</body>
|
</body>
|
||||||
<footer>
|
<footer>
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -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)
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ func (n *Notes) Comment(urlPath string, lineno int, comment string) error {
|
|||||||
writer := io.Writer(f2)
|
writer := io.Writer(f2)
|
||||||
for i := 0; i < lineno+1; i++ {
|
for i := 0; i < lineno+1; i++ {
|
||||||
line, _, err := reader.ReadLine()
|
line, _, err := reader.ReadLine()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -46,14 +49,13 @@ func (n *Notes) Comment(urlPath string, lineno int, comment string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
formatted := "\n"
|
formatted := fmt.Sprintf("> *%s*\n\n", comment)
|
||||||
formatted += fmt.Sprintf("> *%s*\n", comment)
|
|
||||||
_, err = io.Copy(writer, strings.NewReader(formatted))
|
_, err = io.Copy(writer, strings.NewReader(formatted))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = io.Copy(writer, reader)
|
_, err = io.Copy(writer, reader)
|
||||||
if err != nil {
|
if err != nil && err != io.EOF {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f2.Close()
|
f2.Close()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,11 +27,49 @@ func editFile(p filetree.Path) string {
|
|||||||
}
|
}
|
||||||
b, _ := ioutil.ReadFile(p.Local)
|
b, _ := ioutil.ReadFile(p.Local)
|
||||||
return fmt.Sprintf(`
|
return fmt.Sprintf(`
|
||||||
<form action="/submit/%s" method="post" style="width:100%%; height: 90%%">
|
<div class="form">
|
||||||
<table style="width:100%%; height: 90%%">
|
<form action="/submit/%s" method="post">
|
||||||
<textarea name="content" style="width:100%%; min-height:90%%; cursor:crosshair;">%s</textarea>
|
<textarea id="mytext" name="content" style="cursor:crosshair;">%s</textarea>
|
||||||
</table>
|
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
`, href, b)
|
</div>
|
||||||
|
<!--
|
||||||
|
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
@@ -8,15 +8,16 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"local/notes-server/filetree"
|
"local/notes-server/filetree"
|
||||||
"log"
|
"local/notes-server/notes/md"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gomarkdown/markdown"
|
"github.com/fairlyblank/md2min"
|
||||||
"github.com/gomarkdown/markdown/ast"
|
"github.com/gomarkdown/markdown/ast"
|
||||||
"github.com/gomarkdown/markdown/html"
|
"github.com/gomarkdown/markdown/html"
|
||||||
"github.com/gomarkdown/markdown/parser"
|
"github.com/yuin/goldmark"
|
||||||
|
blackfriday "gopkg.in/russross/blackfriday.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (n *Notes) File(urlPath string) (string, error) {
|
func (n *Notes) File(urlPath string) (string, error) {
|
||||||
@@ -25,13 +26,37 @@ func (n *Notes) File(urlPath string) (string, error) {
|
|||||||
return "", errors.New("path is dir")
|
return "", errors.New("path is dir")
|
||||||
}
|
}
|
||||||
b, _ := ioutil.ReadFile(p.Local)
|
b, _ := ioutil.ReadFile(p.Local)
|
||||||
renderer := html.NewRenderer(html.RendererOptions{
|
return n.gomarkdown(urlPath, b)
|
||||||
Flags: html.CommonFlags | html.TOC,
|
return n.blackfriday(urlPath, b)
|
||||||
RenderNodeHook: n.commentFormer(urlPath, b),
|
return n.goldmark(urlPath, b)
|
||||||
|
return n.md2min(urlPath, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notes) blackfriday(urlPath string, b []byte) (string, error) {
|
||||||
|
renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
|
||||||
|
Flags: blackfriday.TOC | blackfriday.Smartypants | blackfriday.SmartypantsFractions | blackfriday.SmartypantsDashes | blackfriday.SmartypantsQuotesNBSP | blackfriday.CompletePage,
|
||||||
})
|
})
|
||||||
parser := parser.NewWithExtensions(parser.CommonExtensions | parser.HeadingIDs | parser.AutoHeadingIDs | parser.Titleblock)
|
return string(blackfriday.Run(
|
||||||
content := markdown.ToHTML(b, parser, renderer)
|
b,
|
||||||
return string(content) + "\n", nil
|
blackfriday.WithRenderer(renderer),
|
||||||
|
)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notes) goldmark(urlPath string, b []byte) (string, error) {
|
||||||
|
buff := bytes.NewBuffer(nil)
|
||||||
|
err := goldmark.Convert(b, buff)
|
||||||
|
return string(buff.Bytes()), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notes) md2min(urlPath string, b []byte) (string, error) {
|
||||||
|
mdc := md2min.New("h1")
|
||||||
|
buff := bytes.NewBuffer(nil)
|
||||||
|
err := mdc.Parse(b, buff)
|
||||||
|
return string(buff.Bytes()), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notes) gomarkdown(urlPath string, b []byte) (string, error) {
|
||||||
|
return md.Gomarkdown(b, n.commentFormer(urlPath, b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notes) commentFormer(urlPath string, md []byte) html.RenderNodeFunc {
|
func (n *Notes) commentFormer(urlPath string, md []byte) html.RenderNodeFunc {
|
||||||
@@ -42,18 +67,21 @@ func (n *Notes) commentFormer(urlPath string, md []byte) html.RenderNodeFunc {
|
|||||||
nextHeader := func() {
|
nextHeader := func() {
|
||||||
cur++
|
cur++
|
||||||
for cur < len(lines) {
|
for cur < len(lines) {
|
||||||
if bytes.Contains(lines[cur], []byte("```")) {
|
for _, opener_closer := range [][]string{{"```", "```"}, {`<summary>`, `</summary>`}} {
|
||||||
|
if cur < len(lines) && bytes.Contains(lines[cur], []byte(opener_closer[0])) {
|
||||||
cur++
|
cur++
|
||||||
for cur < len(lines) && !bytes.Contains(lines[cur], []byte("```")) {
|
for cur < len(lines) && !bytes.Contains(lines[cur], []byte(opener_closer[1])) {
|
||||||
cur++
|
cur++
|
||||||
}
|
}
|
||||||
cur++
|
cur++
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if cur >= len(lines) {
|
if cur >= len(lines) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
line := lines[cur]
|
line := lines[cur]
|
||||||
if ok, err := regexp.Match(`^\s*#+\s*[^\s]+\s*$`, line); err != nil {
|
ok, err := regexp.Match(`^\s*#+\s*.+$`, line)
|
||||||
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
} else if ok {
|
} else if ok {
|
||||||
return
|
return
|
||||||
@@ -62,8 +90,7 @@ func (n *Notes) commentFormer(urlPath string, md []byte) html.RenderNodeFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
|
return func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
|
||||||
if heading, ok := node.(*ast.Heading); ok && !entering {
|
if heading, ok := node.(*ast.Heading); !n.RO && ok && !entering {
|
||||||
log.Printf("%+v", heading)
|
|
||||||
nextHeader()
|
nextHeader()
|
||||||
fmt.Fprintf(w, `
|
fmt.Fprintf(w, `
|
||||||
<form method="POST" action=%q class="comment">
|
<form method="POST" action=%q class="comment">
|
||||||
|
|||||||
35
notes/md/file.go
Executable file
35
notes/md/file.go
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
package md
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gomarkdown/markdown"
|
||||||
|
"github.com/gomarkdown/markdown/html"
|
||||||
|
"github.com/gomarkdown/markdown/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Gomarkdown(b []byte, renderHook html.RenderNodeFunc) (string, error) {
|
||||||
|
renderer := html.NewRenderer(html.RendererOptions{
|
||||||
|
Flags: html.CommonFlags | html.TOC,
|
||||||
|
RenderNodeHook: renderHook,
|
||||||
|
})
|
||||||
|
ext := parser.NoExtensions
|
||||||
|
for _, extension := range []parser.Extensions{
|
||||||
|
parser.NoIntraEmphasis,
|
||||||
|
parser.Tables,
|
||||||
|
parser.FencedCode,
|
||||||
|
parser.Autolink,
|
||||||
|
parser.Strikethrough,
|
||||||
|
parser.SpaceHeadings,
|
||||||
|
parser.HeadingIDs,
|
||||||
|
parser.BackslashLineBreak,
|
||||||
|
parser.DefinitionLists,
|
||||||
|
parser.MathJax,
|
||||||
|
parser.Titleblock,
|
||||||
|
parser.AutoHeadingIDs,
|
||||||
|
parser.Includes,
|
||||||
|
} {
|
||||||
|
ext |= extension
|
||||||
|
}
|
||||||
|
parser := parser.NewWithExtensions(ext)
|
||||||
|
content := markdown.ToHTML(b, parser, renderer)
|
||||||
|
return string(content) + "\n", nil
|
||||||
|
}
|
||||||
@@ -4,10 +4,12 @@ import "local/notes-server/config"
|
|||||||
|
|
||||||
type Notes struct {
|
type Notes struct {
|
||||||
root string
|
root string
|
||||||
|
RO bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Notes {
|
func New() *Notes {
|
||||||
return &Notes{
|
return &Notes{
|
||||||
root: config.Root,
|
root: config.Root,
|
||||||
|
RO: config.ReadOnly,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package notes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html"
|
|
||||||
"local/notes-server/filetree"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (n *Notes) Create(w http.ResponseWriter, r *http.Request) {
|
|
||||||
content := r.FormValue("base")
|
|
||||||
content = html.UnescapeString(content)
|
|
||||||
content = strings.ReplaceAll(content, "\r", "")
|
|
||||||
urlPath := path.Join(r.URL.Path, content)
|
|
||||||
p := filetree.NewPathFromURL(urlPath)
|
|
||||||
if p.IsDir() {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
url := *r.URL
|
|
||||||
url.Path = path.Join("/edit/", p.BaseHREF)
|
|
||||||
http.Redirect(w, r, url.String(), http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package notes
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package notes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
func notesDir(p Path, w http.ResponseWriter, r *http.Request) {
|
|
||||||
dirs, files := lsDir(p)
|
|
||||||
content := dirs.List()
|
|
||||||
notesDirHead(p, w)
|
|
||||||
block(content, w)
|
|
||||||
fmt.Fprintln(w, files.List())
|
|
||||||
}
|
|
||||||
|
|
||||||
func notesDirHead(p Path, w http.ResponseWriter) {
|
|
||||||
fmt.Fprintf(w, `
|
|
||||||
<form action=%q method="get">
|
|
||||||
<input type="text" name="base"></input>
|
|
||||||
<button type="submit">Create</button>
|
|
||||||
</form>
|
|
||||||
`, path.Join("/create/", p.BaseHREF))
|
|
||||||
}
|
|
||||||
|
|
||||||
func lsDir(path Path) (Paths, Paths) {
|
|
||||||
dirs := newDirs()
|
|
||||||
files := newFiles()
|
|
||||||
|
|
||||||
found, _ := ioutil.ReadDir(path.Local)
|
|
||||||
for _, f := range found {
|
|
||||||
dirs.Push(path, f)
|
|
||||||
files.Push(path, f)
|
|
||||||
}
|
|
||||||
return Paths(*dirs), Paths(*files)
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package notes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLsDir(t *testing.T) {
|
|
||||||
p := Path{Local: "/usr/local"}
|
|
||||||
dirs, files := lsDir(p)
|
|
||||||
if len(dirs) == 0 {
|
|
||||||
t.Fatal(len(dirs))
|
|
||||||
}
|
|
||||||
if len(files) == 0 {
|
|
||||||
t.Fatal(len(files))
|
|
||||||
}
|
|
||||||
t.Log(dirs)
|
|
||||||
t.Log(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotesDir(t *testing.T) {
|
|
||||||
path := Path{Local: "/usr/local"}
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
notesDir(path, w, nil)
|
|
||||||
t.Logf("%s", w.Body.Bytes())
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package notes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Server) edit(w http.ResponseWriter, r *http.Request) {
|
|
||||||
p := NewPathFromURL(r.URL.Path)
|
|
||||||
if p.IsDir() {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
head(w, r)
|
|
||||||
editHead(w, p)
|
|
||||||
editFile(w, p)
|
|
||||||
foot(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func editHead(w http.ResponseWriter, p Path) {
|
|
||||||
fmt.Fprintln(w, h2(p.MultiLink()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func editFile(w http.ResponseWriter, p Path) {
|
|
||||||
href := p.HREF
|
|
||||||
href = strings.TrimPrefix(href, "/")
|
|
||||||
hrefs := strings.SplitN(href, "/", 2)
|
|
||||||
href = hrefs[0]
|
|
||||||
if len(hrefs) > 1 {
|
|
||||||
href = hrefs[1]
|
|
||||||
}
|
|
||||||
b, _ := ioutil.ReadFile(p.Local)
|
|
||||||
fmt.Fprintf(w, `
|
|
||||||
<form action="/submit/%s" method="post" style="width:100%%; height: 90%%">
|
|
||||||
<table style="width:100%%; height: 90%%">
|
|
||||||
<textarea name="content" style="width:100%%; min-height:90%%">%s</textarea>
|
|
||||||
</table>
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</form>
|
|
||||||
`, href, b)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package notes
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package notes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/gomarkdown/markdown"
|
|
||||||
"github.com/gomarkdown/markdown/html"
|
|
||||||
"github.com/gomarkdown/markdown/parser"
|
|
||||||
)
|
|
||||||
|
|
||||||
func notesFile(p Path, w http.ResponseWriter, r *http.Request) {
|
|
||||||
b, _ := ioutil.ReadFile(p.Local)
|
|
||||||
notesFileHead(p, w)
|
|
||||||
renderer := html.NewRenderer(html.RendererOptions{
|
|
||||||
Flags: html.CommonFlags | html.TOC,
|
|
||||||
})
|
|
||||||
parser := parser.NewWithExtensions(parser.CommonExtensions | parser.HeadingIDs | parser.AutoHeadingIDs | parser.Titleblock)
|
|
||||||
content := markdown.ToHTML(b, parser, renderer)
|
|
||||||
fmt.Fprintf(w, "%s\n", content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func notesFileHead(p Path, w http.ResponseWriter) {
|
|
||||||
fmt.Fprintf(w, `
|
|
||||||
<a href=%q><input type="button" value="Edit"></input></a>
|
|
||||||
`, path.Join("/edit/", p.BaseHREF))
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package notes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNotesFile(t *testing.T) {
|
|
||||||
f, err := ioutil.TempFile(os.TempDir(), "until*")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
fmt.Fprintln(f, `
|
|
||||||
# Hello
|
|
||||||
## World
|
|
||||||
* This
|
|
||||||
* is
|
|
||||||
* bullets
|
|
||||||
|
|
||||||
| My | table | goes |
|
|
||||||
|----|-------|------|
|
|
||||||
| h | e | n |
|
|
||||||
|
|
||||||
`)
|
|
||||||
f.Close()
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
p := Path{Local: f.Name()}
|
|
||||||
notesFile(p, w, nil)
|
|
||||||
s := string(w.Body.Bytes())
|
|
||||||
shouldContain := []string{
|
|
||||||
"tbody",
|
|
||||||
"h1",
|
|
||||||
"h2",
|
|
||||||
}
|
|
||||||
for _, should := range shouldContain {
|
|
||||||
if !strings.Contains(s, should) {
|
|
||||||
t.Fatalf("%s: %s", should, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.Logf("%s", s)
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package notes
|
|
||||||
|
|
||||||
import "local/notes-server/config"
|
|
||||||
|
|
||||||
type Notes struct {
|
|
||||||
root string
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *Notes {
|
|
||||||
return &Notes{
|
|
||||||
root: config.Root,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package notes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Server) notes(w http.ResponseWriter, r *http.Request) {
|
|
||||||
p := NewPathFromURL(r.URL.Path)
|
|
||||||
if p.IsDir() {
|
|
||||||
head(w, r)
|
|
||||||
notesHead(w, p)
|
|
||||||
notesDir(p, w, r)
|
|
||||||
foot(w, r)
|
|
||||||
} else if p.IsFile() {
|
|
||||||
head(w, r)
|
|
||||||
notesHead(w, p)
|
|
||||||
notesFile(p, w, r)
|
|
||||||
foot(w, r)
|
|
||||||
} else {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func notesHead(w http.ResponseWriter, p Path) {
|
|
||||||
fmt.Fprintln(w, h2(p.MultiLink()))
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package notes
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package notes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"html"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Server) submit(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "POST" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
content := r.FormValue("content")
|
|
||||||
content = html.UnescapeString(content)
|
|
||||||
content = strings.ReplaceAll(content, "\r", "")
|
|
||||||
p := NewPathFromURL(r.URL.Path)
|
|
||||||
os.MkdirAll(path.Dir(p.Local), os.ModePerm)
|
|
||||||
if err := ioutil.WriteFile(p.Local, []byte(content), os.ModePerm); err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
fmt.Fprintln(w, err)
|
|
||||||
} else {
|
|
||||||
url := *r.URL
|
|
||||||
url.Path = path.Join("/notes/", p.BaseHREF)
|
|
||||||
http.Redirect(w, r, url.String(), http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package notes
|
|
||||||
48
server/attach.go
Executable file
48
server/attach.go
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"local/notes-server/filetree"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) attach(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := s._attach(w, r); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) _attach(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
r.ParseMultipartForm(100 << 20)
|
||||||
|
file, handler, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
ft := filetree.NewPathFromURL(r.URL.Path)
|
||||||
|
local := ft.Local
|
||||||
|
target := path.Join(path.Dir(local), "."+path.Base(local)+".attachments", handler.Filename)
|
||||||
|
|
||||||
|
os.MkdirAll(path.Dir(target), os.ModePerm)
|
||||||
|
if fi, err := os.Stat(target); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
} else if err == nil && fi.IsDir() {
|
||||||
|
return errors.New("invalid path")
|
||||||
|
}
|
||||||
|
f, err := os.Create(target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if _, err := io.Copy(f, file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/notes"+strings.TrimPrefix(ft.HREF, "/attach"), http.StatusSeeOther)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package server
|
|||||||
import (
|
import (
|
||||||
"html"
|
"html"
|
||||||
"local/notes-server/filetree"
|
"local/notes-server/filetree"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -11,7 +10,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) comment(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) comment(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Println("COMMAND", r.Method, r.FormValue("lineno"), r.FormValue("content"))
|
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"local/notes-server/filetree"
|
"local/notes-server/filetree"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) notes(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) notes(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -49,13 +51,15 @@ func (s *Server) file(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileHead(w, filetree.NewPathFromURL(r.URL.Path).BaseHREF)
|
s.fileHead(w, r.URL.Path)
|
||||||
fmt.Fprintln(w, file)
|
fmt.Fprintln(w, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileHead(w http.ResponseWriter, baseHREF string) {
|
func (s *Server) fileHead(w http.ResponseWriter, path string) {
|
||||||
|
baseHREF := filetree.NewPathFromURL(path).BaseHREF
|
||||||
htmlEdit(w, baseHREF)
|
htmlEdit(w, baseHREF)
|
||||||
htmlDelete(w, baseHREF)
|
htmlDelete(w, baseHREF)
|
||||||
|
s.htmlAttachments(w, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func htmlEdit(w http.ResponseWriter, baseHREF string) {
|
func htmlEdit(w http.ResponseWriter, baseHREF string) {
|
||||||
@@ -76,6 +80,39 @@ func htmlDelete(w http.ResponseWriter, baseHREF string) {
|
|||||||
</div><br>`, path.Join("/delete/", baseHREF))
|
</div><br>`, path.Join("/delete/", baseHREF))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) htmlAttachments(w http.ResponseWriter, urlPath string) {
|
||||||
|
dir := path.Dir(urlPath)
|
||||||
|
f := "." + path.Base(urlPath) + ".attachments"
|
||||||
|
_, files, _ := s.Notes.Dir(path.Join(dir, f))
|
||||||
|
form := fmt.Sprintf(`
|
||||||
|
<form enctype="multipart/form-data" action="/attach/%s" method="post">
|
||||||
|
<input type="file" name="file" required/>
|
||||||
|
<input type="submit" value="+"/>
|
||||||
|
</form>
|
||||||
|
`, strings.TrimPrefix(urlPath, "/notes/"))
|
||||||
|
if config.ReadOnly {
|
||||||
|
form = ""
|
||||||
|
}
|
||||||
|
lines := strings.Split(files, "\n")
|
||||||
|
for i := range lines {
|
||||||
|
pattern := regexp.MustCompile(`\.(png|jpg|jpeg|gif)">`)
|
||||||
|
if !pattern.MatchString(lines[i]) {
|
||||||
|
lines[i] = strings.ReplaceAll(lines[i], "<a", "<a download")
|
||||||
|
}
|
||||||
|
lines[i] = strings.ReplaceAll(lines[i], `href="/notes`, `href="/raw`)
|
||||||
|
}
|
||||||
|
files = strings.Join(lines, "\n")
|
||||||
|
fmt.Fprintf(w, `<div style='display:inline-block' class="attachments">
|
||||||
|
<details>
|
||||||
|
<summary style="display: flex">
|
||||||
|
<span style="flex-grow: 2">Attachments</span>
|
||||||
|
</summary>
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
</details>
|
||||||
|
</div><br>`, form, files)
|
||||||
|
}
|
||||||
|
|
||||||
func htmlCreate(w http.ResponseWriter, baseHREF string) {
|
func htmlCreate(w http.ResponseWriter, baseHREF string) {
|
||||||
if config.ReadOnly {
|
if config.ReadOnly {
|
||||||
return
|
return
|
||||||
|
|||||||
33
server/raw.go
Executable file
33
server/raw.go
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"local/notes-server/filetree"
|
||||||
|
"local/simpleserve/simpleserve"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) raw(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := s._raw(w, r); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) _raw(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
simpleserve.SetContentTypeIfMedia(w, r)
|
||||||
|
p := filetree.NewPathFromURL(r.URL.Path)
|
||||||
|
if !p.IsFile() {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f, err := os.Open(p.Local)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if _, err := io.Copy(w, f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -18,6 +18,9 @@ func (s *Server) Routes() error {
|
|||||||
"/": {
|
"/": {
|
||||||
handler: s.root,
|
handler: s.root,
|
||||||
},
|
},
|
||||||
|
fmt.Sprintf("raw/%s%s", wildcard, wildcard): {
|
||||||
|
handler: s.gzip(s.authenticate(s.raw)),
|
||||||
|
},
|
||||||
fmt.Sprintf("notes/%s%s", wildcard, wildcard): {
|
fmt.Sprintf("notes/%s%s", wildcard, wildcard): {
|
||||||
handler: s.gzip(s.authenticate(s.notes)),
|
handler: s.gzip(s.authenticate(s.notes)),
|
||||||
},
|
},
|
||||||
@@ -33,6 +36,9 @@ func (s *Server) Routes() error {
|
|||||||
fmt.Sprintf("create/%s%s", wildcard, wildcard): {
|
fmt.Sprintf("create/%s%s", wildcard, wildcard): {
|
||||||
handler: s.gzip(s.authenticate(s.create)),
|
handler: s.gzip(s.authenticate(s.create)),
|
||||||
},
|
},
|
||||||
|
fmt.Sprintf("attach/%s%s", wildcard, wildcard): {
|
||||||
|
handler: s.gzip(s.authenticate(s.attach)),
|
||||||
|
},
|
||||||
fmt.Sprintf("comment/%s%s", wildcard, wildcard): {
|
fmt.Sprintf("comment/%s%s", wildcard, wildcard): {
|
||||||
handler: s.gzip(s.authenticate(s.comment)),
|
handler: s.gzip(s.authenticate(s.comment)),
|
||||||
},
|
},
|
||||||
@@ -43,7 +49,13 @@ func (s *Server) Routes() error {
|
|||||||
|
|
||||||
for path, endpoint := range endpoints {
|
for path, endpoint := range endpoints {
|
||||||
if config.ReadOnly {
|
if config.ReadOnly {
|
||||||
for _, prefix := range []string{"edit/", "delete/", "delete/", "create/", "submit/"} {
|
for _, prefix := range []string{
|
||||||
|
"edit/",
|
||||||
|
"delete/",
|
||||||
|
"create/",
|
||||||
|
"submit/",
|
||||||
|
"attach/",
|
||||||
|
} {
|
||||||
if strings.HasPrefix(path, prefix) {
|
if strings.HasPrefix(path, prefix) {
|
||||||
endpoint.handler = http.NotFound
|
endpoint.handler = http.NotFound
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
38
versions/max_file_size.go
Normal file
38
versions/max_file_size.go
Normal 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
|
||||||
|
`
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user