unstable: initial tags
This commit is contained in:
parent
2e71b60248
commit
b3b62af3a4
28 changed files with 503 additions and 91 deletions
|
|
@ -7,12 +7,16 @@ const (
|
|||
TotpURI = "/totp"
|
||||
LoginURI = "/login"
|
||||
LogoutURI = "/logout"
|
||||
SearchURI = "/search"
|
||||
TagsURI = "/tags"
|
||||
UserURI = "/user"
|
||||
|
||||
RawPrefix = "/raw"
|
||||
IndexPrefix = "/index"
|
||||
UploadPrefix = "/upload"
|
||||
NewPrefix = "/new"
|
||||
MovePrefix = "/move"
|
||||
TagsPrefix = "/tags"
|
||||
DeletePrefix = "/delete"
|
||||
|
||||
Favicon = "/favicon.ico"
|
||||
|
|
|
|||
|
|
@ -14,12 +14,17 @@ type User struct {
|
|||
type Result struct {
|
||||
ID string
|
||||
Path string
|
||||
Keyword string
|
||||
Tags string
|
||||
HTML template.HTML
|
||||
}
|
||||
|
||||
type Results []Result
|
||||
|
||||
type Tag struct {
|
||||
Name string
|
||||
Regex string
|
||||
}
|
||||
|
||||
type Path struct {
|
||||
Name string
|
||||
Abs string
|
||||
|
|
|
|||
|
|
@ -37,7 +37,14 @@ func New(cfg core.Config) (db *DB, err error) {
|
|||
if err != nil && err != store.ErrKeyNotFound {
|
||||
return
|
||||
}
|
||||
err = db.RunMigrations()
|
||||
err = db.store.Get(versionKey, nil)
|
||||
if err == store.ErrKeyNotFound {
|
||||
err = db.addDefaultTags()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = db.runMigrations()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@
|
|||
|
||||
package db
|
||||
|
||||
import "crypto/sha256"
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var nameRe = regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||
|
||||
// Parts taken from strings.TrimPrefix
|
||||
func hasTrimPrefix(s, prefix string) (string, bool) {
|
||||
|
|
|
|||
|
|
@ -49,24 +49,35 @@ func (db *DB) DeleteID(path string) error {
|
|||
return db.store.Delete(idPrefix + path)
|
||||
}
|
||||
|
||||
func (db *DB) NewFile(id, path string) (err error) {
|
||||
func (db *DB) NewFile(id, path string, tags []string) (err error) {
|
||||
err = db.store.Set(idPrefix+path, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.store.Set(pathPrefix+id, path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return db.SetTag(path, tags)
|
||||
}
|
||||
|
||||
func (db *DB) MoveFile(op, np string) error {
|
||||
func (db *DB) MoveFile(op, np string) (err error) {
|
||||
if !db.IsIndexed(op) {
|
||||
return nil
|
||||
}
|
||||
id, err := db.DeleteFile(op)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
return db.NewFile(id, np)
|
||||
tags, err := db.GetTag(op)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.NewFile(id, np, tags)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return db.UnsetTag(op)
|
||||
}
|
||||
|
||||
func (db *DB) DeleteFile(path string) (id string, err error) {
|
||||
|
|
@ -81,6 +92,10 @@ func (db *DB) DeleteFile(path string) (id string, err error) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.UnsetTag(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.DeletePath(id)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,18 +9,25 @@ import (
|
|||
|
||||
const (
|
||||
versionKey = "version/version"
|
||||
currentVersion = 1
|
||||
currentVersion = 2
|
||||
)
|
||||
|
||||
type migrator func(*DB) (int, error)
|
||||
type migrator func(*DB) error
|
||||
|
||||
var migrators = []migrator{
|
||||
func(db *DB) (int, error) {
|
||||
return 0, nil
|
||||
func(db *DB) error {
|
||||
return nil
|
||||
},
|
||||
func(db *DB) error {
|
||||
// dummy placeholder
|
||||
return nil
|
||||
},
|
||||
func(db *DB) error {
|
||||
return db.addDefaultTags()
|
||||
},
|
||||
}
|
||||
|
||||
func (db *DB) RunMigrations() (err error) {
|
||||
func (db *DB) runMigrations() (err error) {
|
||||
var version int
|
||||
err = db.store.Get(versionKey, &version)
|
||||
if err == store.ErrKeyNotFound {
|
||||
|
|
@ -37,11 +44,11 @@ func (db *DB) RunMigrations() (err error) {
|
|||
for version < currentVersion {
|
||||
v := version + 1
|
||||
log.Printf("db: running migration %d", v)
|
||||
version, err = migrators[v](db)
|
||||
err = migrators[v](db)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.store.Set(versionKey, version)
|
||||
err = db.store.Set(versionKey, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
69
pkg/db/tags.go
Normal file
69
pkg/db/tags.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright (C) 2019 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const (
|
||||
tagsPrefix = "tags/"
|
||||
tagPrefix = "tag/"
|
||||
)
|
||||
|
||||
var defaultTags = []core.Tag{
|
||||
{"invoice", `(?i)[^e]rechnung|quittung|beleg|invoice|bill|check`},
|
||||
{"tax", `(?i)steuer|lohn|tax|salary`},
|
||||
{"contract", `(?i)vertrag|kündigung|vereinbarung|contract|termination|agreement`},
|
||||
{"insurance", `(?i)versicherung|versichert|insurance|insured`},
|
||||
}
|
||||
|
||||
func (db *DB) addDefaultTags() (err error) {
|
||||
for _, t := range defaultTags {
|
||||
err = db.NewTag(t.Name, t.Regex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetAllTags() (tags []core.Tag, err error) {
|
||||
err = db.store.ForEachPrefix(tagsPrefix, func(k string, _ []byte) error {
|
||||
var t core.Tag
|
||||
err := db.store.Get(tagsPrefix+k, &t)
|
||||
if err == nil {
|
||||
tags = append(tags, t)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) DeleteTag(name string) error {
|
||||
return db.store.Delete(tagsPrefix + name)
|
||||
}
|
||||
|
||||
func (db *DB) NewTag(name, regex string) (err error) {
|
||||
_, err = regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
name = nameRe.ReplaceAllString(name, "")
|
||||
err = db.store.Set(tagsPrefix+name, core.Tag{name, regex})
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetTag(path string) (tags []string, err error) {
|
||||
err = db.store.Get(tagPrefix+path, &tags)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) SetTag(path string, tags []string) error {
|
||||
return db.store.Set(tagPrefix+path, tags)
|
||||
}
|
||||
|
||||
func (db *DB) UnsetTag(path string) error {
|
||||
return db.store.Delete(tagPrefix + path)
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||
"git.giftfish.de/ston1th/docstore/pkg/otp"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -20,8 +19,6 @@ var (
|
|||
errWrongLogin = errors.New("db: wrong login")
|
||||
errLocked = errors.New("db: user locked")
|
||||
errUserExists = errors.New("db: user already exist")
|
||||
|
||||
userRe = regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||
)
|
||||
|
||||
func (db *DB) UserExists() (b bool) {
|
||||
|
|
@ -53,7 +50,7 @@ func (db *DB) GetUser() (u core.User, err error) {
|
|||
}
|
||||
|
||||
func (db *DB) CreateUser(username, password string) error {
|
||||
username = userRe.ReplaceAllString(username, "")
|
||||
username = nameRe.ReplaceAllString(username, "")
|
||||
if db.UserExists() {
|
||||
return errUserExists
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ var reserved = []string{
|
|||
core.TotpURI,
|
||||
core.LoginURI,
|
||||
core.LogoutURI,
|
||||
core.SearchURI,
|
||||
core.TagsURI,
|
||||
core.UserURI,
|
||||
core.RawPrefix,
|
||||
core.IndexPrefix,
|
||||
core.UploadPrefix,
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
// Copyright (C) 2019 Marius Schellenberger
|
||||
|
||||
package index
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var classes = []class{
|
||||
{regexp.MustCompile(`(?i)[^e]rechnung|quittung|beleg|invoice|bill|check`), []string{"invoice", "rechnung"}},
|
||||
{regexp.MustCompile(`(?i)steuer|lohn|tax|salary`), []string{"tax", "steuer"}},
|
||||
{regexp.MustCompile(`(?i)vertrag|kündigung|vereinbarung|contract|termination|agreement`), []string{"contract", "vertrag"}},
|
||||
{regexp.MustCompile(`(?i)versicherung|versichert|insurance|insured`), []string{"insurance", "versicherung"}},
|
||||
}
|
||||
|
||||
type class struct {
|
||||
r *regexp.Regexp
|
||||
k []string
|
||||
}
|
||||
|
||||
func classify(text string) (keyword []string) {
|
||||
keyword = []string{"", ""}
|
||||
n := 0
|
||||
for _, v := range classes {
|
||||
num := len(v.r.FindAllString(text, -1))
|
||||
if num > n && num >= 2 {
|
||||
keyword = v.k
|
||||
n = num
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ const docType = "document"
|
|||
|
||||
type document struct {
|
||||
Text string `json:"text"`
|
||||
Keyword []string `json:"keyword"`
|
||||
Tags []string `json:"tags"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ func (i *Index) Close() error {
|
|||
return i.i.Close()
|
||||
}
|
||||
|
||||
func (i *Index) Add(text string) (id string, err error) {
|
||||
func (i *Index) Add(text string, tags []string) (id string, err error) {
|
||||
id = newID()
|
||||
for {
|
||||
doc, _ := i.i.Document(id)
|
||||
|
|
@ -70,8 +70,7 @@ func (i *Index) Add(text string) (id string, err error) {
|
|||
}
|
||||
id = newID()
|
||||
}
|
||||
k := classify(text)
|
||||
err = i.i.Index(id, &document{text, k, time.Now()})
|
||||
err = i.i.Index(id, &document{text, tags, time.Now()})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -97,12 +96,12 @@ func (i *Index) Search(search string) (results core.Results, err error) {
|
|||
continue
|
||||
}
|
||||
for fragField, frags := range hit.Fragments {
|
||||
if fragField == "keyword" {
|
||||
var k string
|
||||
if fragField == "tags" {
|
||||
var t string
|
||||
for _, f := range frags {
|
||||
k += f
|
||||
t += f
|
||||
}
|
||||
r.Keyword = strings.ReplaceAll(strings.ReplaceAll(k, "<mark>", ""), "</mark>", "")
|
||||
r.Tags = strings.ReplaceAll(strings.ReplaceAll(t, "<mark>", ""), "</mark>", "")
|
||||
}
|
||||
if fragField == "text" || fragField == "created" {
|
||||
for _, f := range frags {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"git.giftfish.de/ston1th/docstore/pkg/fs"
|
||||
"git.giftfish.de/ston1th/docstore/pkg/log"
|
||||
"git.giftfish.de/ston1th/docstore/pkg/otp"
|
||||
"git.giftfish.de/ston1th/docstore/pkg/tags"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
|
@ -197,12 +198,17 @@ func indexHandler(ctx *Context) {
|
|||
log.Printf("scanner: %s: %s", path, err)
|
||||
return
|
||||
}
|
||||
id, err := ctx.Srv.DB.Index.Add(txt)
|
||||
t, err := ctx.Srv.DB.GetAllTags()
|
||||
if err != nil {
|
||||
log.Printf("getTags: %s: %s", path, err)
|
||||
}
|
||||
found := tags.Find(txt, t)
|
||||
id, err := ctx.Srv.DB.Index.Add(txt, found)
|
||||
if err != nil {
|
||||
log.Printf("index: %s: %s", path, err)
|
||||
return
|
||||
}
|
||||
err = ctx.Srv.DB.NewFile(id, file)
|
||||
err = ctx.Srv.DB.NewFile(id, file, found)
|
||||
if err != nil {
|
||||
log.Printf("newFile: %s: %s", path, err)
|
||||
}
|
||||
|
|
@ -687,6 +693,94 @@ func userTotpHandler(ctx *Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func tagsHandler(ctx *Context) {
|
||||
ctx.Template("tagsHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Tags",
|
||||
BodyTitle: "Tags",
|
||||
}
|
||||
t, err := ctx.Srv.DB.GetAllTags()
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = t
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func tagsEditHandler(ctx *Context) {
|
||||
ctx.Template("tagsEditHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Edit User",
|
||||
BodyTitle: "Edit User",
|
||||
}
|
||||
u, err := ctx.Srv.DB.GetUserWithoutPassword()
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = u
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
|
||||
if password != repeat {
|
||||
ctx.Error("passwords do not match")
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.DB.UpdateUserPassword(password); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("update: %s updated\n", ctx.User())
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func tagsDelHandler(ctx *Context) {
|
||||
ctx.Template("tagsDelHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Edit User",
|
||||
BodyTitle: "Edit User",
|
||||
}
|
||||
u, err := ctx.Srv.DB.GetUserWithoutPassword()
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = u
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
|
||||
if password != repeat {
|
||||
ctx.Error("passwords do not match")
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.DB.UpdateUserPassword(password); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("update: %s updated\n", ctx.User())
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func logoutHandler(ctx *Context) {
|
||||
user := ctx.User()
|
||||
if user == "" {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ var prefixRoutes = []route{
|
|||
|
||||
var routes = append(static, []route{
|
||||
{
|
||||
"/search",
|
||||
core.SearchURI,
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
searchHandler)),
|
||||
|
|
@ -122,6 +122,27 @@ var routes = append(static, []route{
|
|||
userTotpHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/tags",
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
tagsHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/tags/{tag:[a-zA-Z0-9]+}/edit",
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
tagsEditHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/tags/{tag:[a-zA-Z0-9]+}/del",
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
tagsDelHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.LogoutURI,
|
||||
jwtHandler(
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ const (
|
|||
<a href="/index{{.Data.Name}}" class="btn btn-sm btn-success">Index</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<a target="_blank" href="{{.Data.Abs}}" class="btn btn-sm btn-primary">Raw</a>
|
||||
<a href="/move{{.Data.Name}}" class="btn btn-sm btn-primary">Move</a>
|
||||
<a href="/delete{{.Data.Name}}" class="btn btn-sm btn-danger">Delete</a>
|
||||
</div>
|
||||
|
|
@ -159,7 +160,7 @@ const (
|
|||
<title>DocStore | {{.Title}}</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link>
|
||||
<link rel="stylesheet" type="text/css" href="/bootstrap.css" media="screen" integrity="sha256-3YeveuySvBgp2GItUS8eV1R/9T1zcPMyvw22SyVf8qo="></link>
|
||||
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-VMvzVve7KY7ia4G8uBrASNEZ/Qc/5jCqVV3WWU+Y9ao="></link>
|
||||
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-NLGEBzpF8t2UDEPzWyOYGGP/HWLNb29ar1MdYlwJroQ="></link>
|
||||
<meta name="theme-color" content="#375a7f">
|
||||
<meta name="description" content="{{.Title}}">
|
||||
</head>
|
||||
|
|
@ -265,6 +266,9 @@ const (
|
|||
menu = `{{define "menu"}}
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item"><a class="nav-link" href="/">Root</a></li>
|
||||
{{if .Login}}
|
||||
<li class="nav-item"><a class="nav-link" href="/tags">Tags</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<form class="form-inline" method="post" action="/search">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
|
|
@ -426,6 +430,86 @@ const (
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}`
|
||||
tagsDel = `{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/tags/{{.Data.Name}}/edit" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="name">Name</label>
|
||||
<input class="form-control input-sm" type="text" id="name" name="name" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="regex">Regex</label>
|
||||
<input class="form-control input-sm" type="text" id="regex" name="regex" value="{{.Data.Regex}}">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||
<a href="/tags" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}`
|
||||
tagsEdit = `{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/tags/{{.Data.Name}}/edit" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="name">Name</label>
|
||||
<input class="form-control input-sm" type="text" id="name" name="name" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="regex">Regex</label>
|
||||
<input class="form-control input-sm" type="text" id="regex" name="regex" value="{{.Data.Regex}}">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||
<a href="/tags" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}`
|
||||
tagsList = `{{define "body"}}
|
||||
<div class="row">
|
||||
<table class="table table-stripped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Regex</th>
|
||||
<th>Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{if .Data}}
|
||||
{{range $item := .Data}}
|
||||
{{$len := len $item.Regex}}
|
||||
<tr>
|
||||
<td>{{$item.Name}}</td>
|
||||
<td><code>{{printf "%.50s" $item.Regex}}{{if gt $len 50}}...{{end}}</code></td>
|
||||
<td><div class="btn-group">
|
||||
<a href="/tags/{{$item.Name}}/edit" class="btn btn-sm btn-primary">Edit</a>
|
||||
<a href="/tags/{{$item.Name}}/del" class="btn btn-sm btn-danger">Delete</a>
|
||||
</div></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}`
|
||||
upload = `{{define "body"}}
|
||||
<div class="row">
|
||||
|
|
@ -579,7 +663,7 @@ a.anchor {
|
|||
top: -70px;
|
||||
visibility: hidden;
|
||||
}
|
||||
a.mono {
|
||||
.mono {
|
||||
font-family: monospace;
|
||||
}
|
||||
.alert-primary {
|
||||
|
|
@ -744,6 +828,10 @@ func (s *HTTPServer) loadTemplates() {
|
|||
s.templ["userEditHandler"] = parse(index, menu, userEdit)
|
||||
// totp
|
||||
s.templ["userTotpHandler"] = parse(index, menu, userTotp)
|
||||
// tags
|
||||
s.templ["tagsHandler"] = parse(index, menu, tagsList)
|
||||
s.templ["tagsEditHandler"] = parse(index, menu, tagsEdit)
|
||||
s.templ["tagsDelHandler"] = parse(index, menu, tagsDel)
|
||||
|
||||
if fatal {
|
||||
log.Fatal("parse: ", errors.New("parsing templates failed"))
|
||||
|
|
|
|||
|
|
@ -119,6 +119,15 @@ func (bs *BoltStore) ForEach(f func(string, []byte) error) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) ForEachPrefix(prefix string, f func(string, []byte) error) error {
|
||||
return bs.ForEach(func(k string, v []byte) error {
|
||||
if trim, ok := hasTrimPrefix(k, prefix); ok {
|
||||
return f(trim, v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Delete(key string) error {
|
||||
return bs.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket([]byte(defaultBoltBucket)).Delete([]byte(key))
|
||||
|
|
|
|||
25
pkg/store/gob.go
Normal file
25
pkg/store/gob.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (C) 2019 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
)
|
||||
|
||||
type gobMarshaler struct{}
|
||||
|
||||
func NewGOB() Marshaler {
|
||||
return gobMarshaler{}
|
||||
}
|
||||
|
||||
func (gobMarshaler) Marshal(v interface{}) (b []byte, err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err = gob.NewEncoder(buf).Encode(v)
|
||||
b = buf.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func (gobMarshaler) Unmarshal(data []byte, v interface{}) error {
|
||||
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v)
|
||||
}
|
||||
11
pkg/store/helper.go
Normal file
11
pkg/store/helper.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (C) 2019 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
// Parts taken from strings.TrimPrefix
|
||||
func hasTrimPrefix(s, prefix string) (string, bool) {
|
||||
if len(s) >= len(prefix) && s[0:len(prefix)] == prefix {
|
||||
return s[len(prefix):], true
|
||||
}
|
||||
return s, false
|
||||
}
|
||||
|
|
@ -2,29 +2,7 @@
|
|||
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
)
|
||||
|
||||
type Marshaler interface {
|
||||
Marshal(v interface{}) ([]byte, error)
|
||||
Unmarshal(data []byte, v interface{}) error
|
||||
}
|
||||
|
||||
type gobMarshaler struct{}
|
||||
|
||||
func NewGOB() Marshaler {
|
||||
return gobMarshaler{}
|
||||
}
|
||||
|
||||
func (gobMarshaler) Marshal(v interface{}) (b []byte, err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err = gob.NewEncoder(buf).Encode(v)
|
||||
b = buf.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func (gobMarshaler) Unmarshal(data []byte, v interface{}) error {
|
||||
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ type Store interface {
|
|||
Get(string, interface{}) error
|
||||
Set(string, interface{}) error
|
||||
ForEach(func(string, []byte) error) error
|
||||
ForEachPrefix(string, func(string, []byte) error) error
|
||||
Delete(string) error
|
||||
Close() error
|
||||
}
|
||||
|
|
|
|||
18
pkg/tags/tags.go
Normal file
18
pkg/tags/tags.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (C) 2019 Marius Schellenberger
|
||||
|
||||
package tags
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func Find(text string, tags []core.Tag) (found []string) {
|
||||
for _, t := range tags {
|
||||
re := regexp.MustCompile(t.Regex)
|
||||
if re.MatchString(text) {
|
||||
found = append(found, t.Name)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -96,6 +96,10 @@ func (s *HTTPServer) loadTemplates() {
|
|||
s.templ["userEditHandler"] = parse(index, menu, userEdit)
|
||||
// totp
|
||||
s.templ["userTotpHandler"] = parse(index, menu, userTotp)
|
||||
// tags
|
||||
s.templ["tagsHandler"] = parse(index, menu, tagsList)
|
||||
s.templ["tagsEditHandler"] = parse(index, menu, tagsEdit)
|
||||
s.templ["tagsDelHandler"] = parse(index, menu, tagsDel)
|
||||
|
||||
if fatal {
|
||||
log.Fatal("parse: ", errors.New("parsing templates failed"))
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ a.anchor {
|
|||
top: -70px;
|
||||
visibility: hidden;
|
||||
}
|
||||
a.mono {
|
||||
.mono {
|
||||
font-family: monospace;
|
||||
}
|
||||
.alert-primary {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
<a href="/index{{.Data.Name}}" class="btn btn-sm btn-success">Index</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<a target="_blank" href="{{.Data.Abs}}" class="btn btn-sm btn-primary">Raw</a>
|
||||
<a href="/move{{.Data.Name}}" class="btn btn-sm btn-primary">Move</a>
|
||||
<a href="/delete{{.Data.Name}}" class="btn btn-sm btn-danger">Delete</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<title>DocStore | {{.Title}}</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link>
|
||||
<link rel="stylesheet" type="text/css" href="/bootstrap.css" media="screen" integrity="sha256-3YeveuySvBgp2GItUS8eV1R/9T1zcPMyvw22SyVf8qo="></link>
|
||||
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-VMvzVve7KY7ia4G8uBrASNEZ/Qc/5jCqVV3WWU+Y9ao="></link>
|
||||
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-NLGEBzpF8t2UDEPzWyOYGGP/HWLNb29ar1MdYlwJroQ="></link>
|
||||
<meta name="theme-color" content="#375a7f">
|
||||
<meta name="description" content="{{.Title}}">
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
{{define "menu"}}
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item"><a class="nav-link" href="/">Root</a></li>
|
||||
{{if .Login}}
|
||||
<li class="nav-item"><a class="nav-link" href="/tags">Tags</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<form class="form-inline" method="post" action="/search">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
|
|
|
|||
26
templates/tagsDel.html
Normal file
26
templates/tagsDel.html
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/tags/{{.Data.Name}}/edit" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="name">Name</label>
|
||||
<input class="form-control input-sm" type="text" id="name" name="name" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="regex">Regex</label>
|
||||
<input class="form-control input-sm" type="text" id="regex" name="regex" value="{{.Data.Regex}}">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||
<a href="/tags" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
26
templates/tagsEdit.html
Normal file
26
templates/tagsEdit.html
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/tags/{{.Data.Name}}/edit" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="name">Name</label>
|
||||
<input class="form-control input-sm" type="text" id="name" name="name" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="regex">Regex</label>
|
||||
<input class="form-control input-sm" type="text" id="regex" name="regex" value="{{.Data.Regex}}">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||
<a href="/tags" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
28
templates/tagsList.html
Normal file
28
templates/tagsList.html
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<table class="table table-stripped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Regex</th>
|
||||
<th>Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{if .Data}}
|
||||
{{range $item := .Data}}
|
||||
{{$len := len $item.Regex}}
|
||||
<tr>
|
||||
<td>{{$item.Name}}</td>
|
||||
<td><code>{{printf "%.50s" $item.Regex}}{{if gt $len 50}}...{{end}}</code></td>
|
||||
<td><div class="btn-group">
|
||||
<a href="/tags/{{$item.Name}}/edit" class="btn btn-sm btn-primary">Edit</a>
|
||||
<a href="/tags/{{$item.Name}}/del" class="btn btn-sm btn-danger">Delete</a>
|
||||
</div></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
Loading…
Add table
Add a link
Reference in a new issue