some improvements
This commit is contained in:
parent
3c5be25cb3
commit
58a0dc36d5
25 changed files with 233 additions and 105 deletions
4
Makefile
4
Makefile
|
|
@ -1,7 +1,7 @@
|
||||||
CC=go
|
CC=go
|
||||||
BUILD=build -v
|
BUILD=build -v
|
||||||
VERSION=$(shell cat VERSION)
|
VERSION=$(shell cat VERSION)
|
||||||
GCFLAGS=-gcflags '-e'
|
GCFLAGS=-gcflags 'all=-e'
|
||||||
LDFLAGS=-ldflags '-X main.version=$(VERSION) -s -w'
|
LDFLAGS=-ldflags '-X main.version=$(VERSION) -s -w'
|
||||||
PROGRAM=docstore
|
PROGRAM=docstore
|
||||||
ENV=CGO_ENABLED=0 GO111MODULE=on
|
ENV=CGO_ENABLED=0 GO111MODULE=on
|
||||||
|
|
@ -21,7 +21,7 @@ vendor: clean generate gofmt
|
||||||
$(ENV) $(CC) $(BUILD) -mod=vendor $(LDFLAGS)
|
$(ENV) $(CC) $(BUILD) -mod=vendor $(LDFLAGS)
|
||||||
|
|
||||||
$(PROGRAM): clean generate gofmt
|
$(PROGRAM): clean generate gofmt
|
||||||
$(ENV) $(CC) $(BUILD) $(LDFLAGS)
|
$(ENV) $(CC) $(BUILD) $(GCFLAGS) $(LDFLAGS)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(CC) clean -x
|
$(CC) clean -x
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,9 @@ See the `scripts/` directory on how to install docstore on Linux or OpenBSD.
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Fulltext indexing of documents
|
* Fulltext indexing of documents
|
||||||
|
* The `Index All` button tries add every file not already indexed to the index
|
||||||
* Custom regex based tags (see https://golang.org/pkg/regexp/syntax/)
|
* Custom regex based tags (see https://golang.org/pkg/regexp/syntax/)
|
||||||
|
* The `Retag All` button will reapply the current tags to all indexed documents
|
||||||
* Password hashing using BCrypt
|
* Password hashing using BCrypt
|
||||||
* 2FA using TOTP
|
* 2FA using TOTP
|
||||||
* Content Security Policy
|
* Content Security Policy
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@ const (
|
||||||
TagsURI = "/tags"
|
TagsURI = "/tags"
|
||||||
UserURI = "/user"
|
UserURI = "/user"
|
||||||
|
|
||||||
RawPrefix = "/raw"
|
RawPrefix = "/raw"
|
||||||
IndexPrefix = "/index"
|
IndexPrefix = "/index"
|
||||||
ReindexPrefix = "/reindex"
|
RetagPrefix = "/retag"
|
||||||
UploadPrefix = "/upload"
|
UploadPrefix = "/upload"
|
||||||
NewPrefix = "/new"
|
NewPrefix = "/new"
|
||||||
MovePrefix = "/move"
|
MovePrefix = "/move"
|
||||||
TagsPrefix = "/tags"
|
TagsPrefix = "/tags"
|
||||||
DeletePrefix = "/delete"
|
DeletePrefix = "/delete"
|
||||||
|
|
||||||
Favicon = "/favicon.ico"
|
Favicon = "/favicon.ico"
|
||||||
BootstrapCSS = "/bootstrap.css"
|
BootstrapCSS = "/bootstrap.css"
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ type Tag struct {
|
||||||
|
|
||||||
type Tags []Tag
|
type Tags []Tag
|
||||||
|
|
||||||
|
func (t Tags) Len() int { return len(t) }
|
||||||
|
func (t Tags) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
||||||
|
func (t Tags) Less(i, j int) bool { return t[i].Name < t[j].Name }
|
||||||
|
|
||||||
type RTag struct {
|
type RTag struct {
|
||||||
Name string
|
Name string
|
||||||
Regex *regexp.Regexp
|
Regex *regexp.Regexp
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const (
|
||||||
pathPrefix = "path/"
|
pathPrefix = "path/"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (db *DB) Reindex(path string, tags core.RTags) (err error) {
|
func (db *DB) Retag(path string, tags core.RTags) (err error) {
|
||||||
if tags == nil {
|
if tags == nil {
|
||||||
tags, err = db.GetAllRTags()
|
tags, err = db.GetAllRTags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -30,20 +30,20 @@ func (db *DB) Reindex(path string, tags core.RTags) (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Debugf("db: reindexing: %s", path)
|
log.Debugf("db: retagging: %s", path)
|
||||||
doc, err := db.Index.Get(id)
|
doc, err := db.Index.Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
doc.Tags = tags.Match(doc.Text)
|
doc.Tags = tags.Match(doc.Text)
|
||||||
err = db.Index.Reindex(id, doc)
|
err = db.Index.Update(id, doc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return db.SetTag(path, doc.Tags)
|
return db.SetTag(path, doc.Tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) ReindexAll() (err error) {
|
func (db *DB) RetagAll() (err error) {
|
||||||
var paths []string
|
var paths []string
|
||||||
err = db.store.ForEachPrefix(idPrefix, func(k string, _ []byte) error {
|
err = db.store.ForEachPrefix(idPrefix, func(k string, _ []byte) error {
|
||||||
paths = append(paths, k)
|
paths = append(paths, k)
|
||||||
|
|
@ -57,12 +57,12 @@ func (db *DB) ReindexAll() (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, p := range paths {
|
for _, p := range paths {
|
||||||
e := db.Reindex(p, tags)
|
e := db.Retag(p, tags)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = errors.New("reindexAll had errors")
|
err = errors.New("retagAll had errors")
|
||||||
}
|
}
|
||||||
log.Printf("db: error reindexing %s: %s", p, e)
|
log.Printf("db: error retagging %s: %s", p, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ var migrators = []migrator{
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return db.ReindexAll()
|
return db.RetagAll()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ package db
|
||||||
import (
|
import (
|
||||||
"git.giftfish.de/ston1th/docstore/pkg/core"
|
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -38,6 +39,7 @@ func (db *DB) GetAllTags() (tags core.Tags, err error) {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
sort.Sort(tags)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ type Directory struct {
|
||||||
|
|
||||||
type Filesystem struct {
|
type Filesystem struct {
|
||||||
Base string
|
Base string
|
||||||
Log *log.ScanLog
|
|
||||||
|
|
||||||
// protects scan
|
// protects scan
|
||||||
m sync.RWMutex
|
m sync.RWMutex
|
||||||
|
|
@ -54,7 +53,7 @@ func NewFilesystem(base string) (fs *Filesystem, err error) {
|
||||||
err = errors.New("base dir '" + base + "' is not a directory")
|
err = errors.New("base dir '" + base + "' is not a directory")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fs = &Filesystem{Base: base, Log: log.NewScanLog(0), scan: make(map[string]struct{})}
|
fs = &Filesystem{Base: base, scan: make(map[string]struct{})}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ var reserved = []string{
|
||||||
core.UserURI,
|
core.UserURI,
|
||||||
core.RawPrefix,
|
core.RawPrefix,
|
||||||
core.IndexPrefix,
|
core.IndexPrefix,
|
||||||
core.ReindexPrefix,
|
core.RetagPrefix,
|
||||||
core.UploadPrefix,
|
core.UploadPrefix,
|
||||||
core.NewPrefix,
|
core.NewPrefix,
|
||||||
core.MovePrefix,
|
core.MovePrefix,
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ func (i *Index) Add(text string, tags []string) (id string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Index) Reindex(id string, doc *Document) error {
|
func (i *Index) Update(id string, doc *Document) error {
|
||||||
return i.i.Index(id, doc)
|
return i.i.Index(id, doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,24 +3,52 @@
|
||||||
package scan
|
package scan
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
pdfContentType = "application/pdf"
|
pdfT = "application/pdf"
|
||||||
maxSniffSize = 512
|
jpegT = "image/jpeg"
|
||||||
|
pngT = "image/png"
|
||||||
|
bmpT = "image/bmp"
|
||||||
|
gifT = "image/gif"
|
||||||
|
webpT = "image/webp"
|
||||||
|
pnmT = "image/x-portable-anymap"
|
||||||
|
jp2T = "image/jp2"
|
||||||
|
tiffT = "image/tiff"
|
||||||
|
maxSniffSize = 512
|
||||||
)
|
)
|
||||||
|
|
||||||
func isPDF(path string) bool {
|
type extendedSig struct {
|
||||||
// TODO
|
sig []byte
|
||||||
// return checkContentType(path) || checkExtension(path)
|
ct string
|
||||||
return checkContentType(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkContentType(path string) bool {
|
var extendedSigs = []extendedSig{
|
||||||
|
{[]byte("P1\x0A"), pnmT},
|
||||||
|
{[]byte("P2\x0A"), pnmT},
|
||||||
|
{[]byte("P3\x0A"), pnmT},
|
||||||
|
{[]byte("P4\x0A"), pnmT},
|
||||||
|
{[]byte("P5\x0A"), pnmT},
|
||||||
|
{[]byte("P6\x0A"), pnmT},
|
||||||
|
{[]byte("P7\x0A"), pnmT},
|
||||||
|
{[]byte("\x00\x00\x00\x0C\x6A\x50\x20\x20\x0D\x0A\x87\x0A\x00\x00\x00\x14\x66\x74\x79\x70\x6A\x70\x32"), jp2T},
|
||||||
|
{[]byte("\x4d\x4d\x00\x2a"), tiffT},
|
||||||
|
{[]byte("\x49\x49\x2a\x00"), tiffT},
|
||||||
|
}
|
||||||
|
|
||||||
|
func extendedContentType(buf []byte) string {
|
||||||
|
for _, sig := range extendedSigs {
|
||||||
|
if bytes.HasPrefix(buf, sig.sig) {
|
||||||
|
return sig.ct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPDF(path string) bool {
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
|
|
@ -31,9 +59,28 @@ func checkContentType(path string) bool {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return http.DetectContentType(buf) == pdfContentType
|
return http.DetectContentType(buf) == pdfT
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkExtension(path string) bool {
|
func isImage(path string) bool {
|
||||||
return strings.ToLower(filepath.Ext(path)) == pdfExt
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
buf := make([]byte, maxSniffSize)
|
||||||
|
_, err = f.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch http.DetectContentType(buf) {
|
||||||
|
case jpegT, pngT, bmpT, gifT, webpT:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
switch extendedContentType(buf) {
|
||||||
|
case pnmT, jp2T, tiffT:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,10 @@ func (s *Scanner) Scan(file string) (filename, text string, err error) {
|
||||||
var pdffile string
|
var pdffile string
|
||||||
pdf := isPDF(scanfile)
|
pdf := isPDF(scanfile)
|
||||||
if !pdf {
|
if !pdf {
|
||||||
|
if !isImage(scanfile) {
|
||||||
|
err = errors.New("error: input file is not an image")
|
||||||
|
return
|
||||||
|
}
|
||||||
var (
|
var (
|
||||||
pdf []byte
|
pdf []byte
|
||||||
f *os.File
|
f *os.File
|
||||||
|
|
|
||||||
|
|
@ -69,13 +69,14 @@ type webData struct {
|
||||||
|
|
||||||
NotFound string
|
NotFound string
|
||||||
|
|
||||||
User string
|
User string
|
||||||
Token string
|
Token string
|
||||||
Msg string
|
Msg string
|
||||||
Search string
|
Query string
|
||||||
|
|
||||||
Path string
|
Path string
|
||||||
Paths core.Paths
|
Paths core.Paths
|
||||||
|
Tags []string
|
||||||
Data interface{}
|
Data interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,6 +210,10 @@ func (c *Context) Form(name string) string {
|
||||||
return c.Request.PostFormValue(name)
|
return c.Request.PostFormValue(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetForm(name string) string {
|
||||||
|
return c.Request.FormValue(name)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Context) Var(name string) (ret string) {
|
func (c *Context) Var(name string) (ret string) {
|
||||||
ret, _ = mux.Vars(c.Request)[name]
|
ret, _ = mux.Vars(c.Request)[name]
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -173,30 +173,30 @@ func rawHandler(ctx *Context) {
|
||||||
http.ServeContent(ctx.Response, ctx.Request, fi.Name(), fi.ModTime(), f)
|
http.ServeContent(ctx.Response, ctx.Request, fi.Name(), fi.ModTime(), f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scanFile(ctx *Context, path string) (err error) {
|
func scanFile(srv *HTTPServer, path string) (err error) {
|
||||||
err = ctx.Srv.FS.AddScan(path)
|
err = srv.FS.AddScan(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer ctx.Srv.FS.RemoveScan(path)
|
defer srv.FS.RemoveScan(path)
|
||||||
l := ctx.Srv.FS.Log
|
l := srv.Log
|
||||||
file, txt, err := ctx.Srv.Scanner.Scan(path)
|
file, txt, err := srv.Scanner.Scan(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Printf("scanner: %s: %s", path, err)
|
l.Printf("scanner: %s: %s", path, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tags, err := ctx.Srv.DB.GetAllRTags()
|
tags, err := srv.DB.GetAllRTags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Printf("getTags: %s: %s", path, err)
|
l.Printf("getTags: %s: %s", path, err)
|
||||||
}
|
}
|
||||||
found := tags.Match(txt)
|
found := tags.Match(txt)
|
||||||
id, err := ctx.Srv.DB.Index.Add(txt, found)
|
id, err := srv.DB.Index.Add(txt, found)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Printf("index: %s: %s", path, err)
|
l.Printf("index: %s: %s", path, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = ctx.Srv.DB.NewFile(id, file, found)
|
err = srv.DB.NewFile(id, file, found)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Printf("newFile: %s: %s", path, err)
|
l.Printf("newFile: %s: %s", path, err)
|
||||||
}
|
}
|
||||||
|
|
@ -214,14 +214,19 @@ func indexHandler(ctx *Context) {
|
||||||
path := ctx.Path()
|
path := ctx.Path()
|
||||||
if strings.HasPrefix(path, core.IndexPrefix) {
|
if strings.HasPrefix(path, core.IndexPrefix) {
|
||||||
fpath = strings.TrimPrefix(path, core.IndexPrefix)
|
fpath = strings.TrimPrefix(path, core.IndexPrefix)
|
||||||
err = scanFile(ctx, fpath)
|
err = scanFile(ctx.Srv, fpath)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(path, core.ReindexPrefix) {
|
if strings.HasPrefix(path, core.RetagPrefix) {
|
||||||
fpath = strings.TrimPrefix(path, core.ReindexPrefix)
|
fpath = strings.TrimPrefix(path, core.RetagPrefix)
|
||||||
if fpath == "" {
|
if fpath == "" {
|
||||||
err = ctx.Srv.DB.ReindexAll()
|
go func() {
|
||||||
|
err := ctx.Srv.DB.RetagAll()
|
||||||
|
if err != nil {
|
||||||
|
ctx.Srv.Log.Printf("retagAll: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
} else {
|
} else {
|
||||||
err = ctx.Srv.DB.Reindex(fpath, nil)
|
err = ctx.Srv.DB.Retag(fpath, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p, _ := filepath.Split(fpath)
|
p, _ := filepath.Split(fpath)
|
||||||
|
|
@ -237,6 +242,19 @@ func indexHandler(ctx *Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Redirect(p, http.StatusFound)
|
ctx.Redirect(p, http.StatusFound)
|
||||||
|
case "POST":
|
||||||
|
if ctx.Path() != core.IndexPrefix+"/all" || !ctx.CheckXsrf() {
|
||||||
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for _, f := range ctx.Srv.FS.RecursiveFiles("/") {
|
||||||
|
if !ctx.Srv.DB.IsIndexed(f) {
|
||||||
|
scanFile(ctx.Srv, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -457,13 +475,21 @@ func dirHandler(ctx *Context) {
|
||||||
Title: "Document Viewer | " + path,
|
Title: "Document Viewer | " + path,
|
||||||
}
|
}
|
||||||
ctx.Template("fileHandler")
|
ctx.Template("fileHandler")
|
||||||
|
f := ctx.Srv.DB.IsIndexed(path)
|
||||||
ctx.Data.Data = core.Path{
|
ctx.Data.Data = core.Path{
|
||||||
Name: path,
|
Name: path,
|
||||||
Abs: core.RawPrefix + path,
|
Abs: core.RawPrefix + path,
|
||||||
Flag: ctx.Srv.DB.IsIndexed(path),
|
Flag: f,
|
||||||
Scan: ctx.Srv.FS.CheckScan(path),
|
Scan: ctx.Srv.FS.CheckScan(path),
|
||||||
}
|
}
|
||||||
ctx.Data.Paths = fs.Paths(path)
|
ctx.Data.Paths = fs.Paths(path)
|
||||||
|
if f {
|
||||||
|
ctx.Data.Tags, err = ctx.Srv.DB.GetTag(path)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.Exec()
|
ctx.Exec()
|
||||||
default:
|
default:
|
||||||
p := fs.Clean(path)
|
p := fs.Clean(path)
|
||||||
|
|
@ -573,19 +599,18 @@ func searchHandler(ctx *Context) {
|
||||||
Title: "Search",
|
Title: "Search",
|
||||||
BodyTitle: "Search",
|
BodyTitle: "Search",
|
||||||
}
|
}
|
||||||
switch ctx.Method() {
|
m := ctx.Method()
|
||||||
case "GET":
|
switch m {
|
||||||
ctx.Exec()
|
case "GET", "POST":
|
||||||
case "POST":
|
if m == "POST" && !ctx.CheckXsrf() {
|
||||||
if !ctx.CheckXsrf() {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s := ctx.Form("search")
|
q := ctx.GetForm("query")
|
||||||
ctx.Data.Search = s
|
ctx.Data.Query = q
|
||||||
if s == "" {
|
if q == "" {
|
||||||
s = `""`
|
q = `""`
|
||||||
}
|
}
|
||||||
res, err := ctx.Srv.DB.Index.Search(s)
|
res, err := ctx.Srv.DB.Index.Search(q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
|
|
@ -706,7 +731,7 @@ func logsHandler(ctx *Context) {
|
||||||
}
|
}
|
||||||
switch ctx.Method() {
|
switch ctx.Method() {
|
||||||
case "GET":
|
case "GET":
|
||||||
ctx.Data.Data = ctx.Srv.FS.Log.Logs()
|
ctx.Data.Data = ctx.Srv.Log.Logs()
|
||||||
ctx.Exec()
|
ctx.Exec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,10 @@ var prefixRoutes = []route{
|
||||||
jwtHandler(
|
jwtHandler(
|
||||||
authHandler(
|
authHandler(
|
||||||
indexHandler)),
|
indexHandler)),
|
||||||
[]string{"GET"},
|
[]string{"GET", "POST"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
core.ReindexPrefix,
|
core.RetagPrefix,
|
||||||
jwtHandler(
|
jwtHandler(
|
||||||
authHandler(
|
authHandler(
|
||||||
indexHandler)),
|
indexHandler)),
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ type HTTPServer struct {
|
||||||
JWT *jwt.JWT
|
JWT *jwt.JWT
|
||||||
Scanner *scan.Scanner
|
Scanner *scan.Scanner
|
||||||
FS *fs.Filesystem
|
FS *fs.Filesystem
|
||||||
|
Log *log.ScanLog
|
||||||
|
|
||||||
templ map[string]*template.Template
|
templ map[string]*template.Template
|
||||||
res map[string][]byte
|
res map[string][]byte
|
||||||
|
|
@ -45,6 +46,7 @@ func NewHTTPServer(cfg core.Config, l net.Listener, s *scan.Scanner) (srv *HTTPS
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
listener: l,
|
listener: l,
|
||||||
Scanner: s,
|
Scanner: s,
|
||||||
|
Log: log.NewScanLog(0),
|
||||||
}
|
}
|
||||||
srv.handler = srv.buildRoutes()
|
srv.handler = srv.buildRoutes()
|
||||||
srv.loadTemplates()
|
srv.loadTemplates()
|
||||||
|
|
|
||||||
|
|
@ -46,17 +46,21 @@ const (
|
||||||
<li class="breadcrumb-item{{if $item.Flag}} active{{end}}">{{if $item.Flag}}{{$item.Name}}{{else}}<a href="{{$item.Abs}}">{{$item.Name}}</a>{{end}}</li>
|
<li class="breadcrumb-item{{if $item.Flag}} active{{end}}">{{if $item.Flag}}{{$item.Name}}{{else}}<a href="{{$item.Abs}}">{{$item.Name}}</a>{{end}}</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ol>
|
</ol>
|
||||||
<div class="btn-group btn-breadcrumb">
|
<form action="/index/all" method="post">
|
||||||
<a href="/upload{{.Path}}" class="btn btn-sm btn-success">Upload</a>
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<a href="/new{{.Path}}" class="btn btn-sm btn-primary">New Directory</a>
|
<div class="btn-group btn-breadcrumb">
|
||||||
{{$pl := len .Paths}}
|
<a href="/upload{{.Path}}" class="btn btn-sm btn-success">Upload</a>
|
||||||
{{if eq $pl 0}}
|
<a href="/new{{.Path}}" class="btn btn-sm btn-primary">New Directory</a>
|
||||||
<a href="/reindex" class="btn btn-sm btn-warning">Reindex All</a>
|
{{$pl := len .Paths}}
|
||||||
{{else}}
|
{{if eq $pl 0}}
|
||||||
<a href="/move{{.Path}}" class="btn btn-sm btn-primary">Move</a>
|
<a href="/retag" class="btn btn-sm btn-warning">Retag All</a>
|
||||||
<a href="/delete{{.Path}}" class="btn btn-sm btn-danger">Delete</a>
|
<button class="btn btn-sm btn-warning" type="submit">Index All</button>
|
||||||
{{end}}
|
{{else}}
|
||||||
</div>
|
<a href="/move{{.Path}}" class="btn btn-sm btn-primary">Move</a>
|
||||||
|
<a href="/delete{{.Path}}" class="btn btn-sm btn-danger">Delete</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
@ -88,7 +92,7 @@ const (
|
||||||
<td>File</td>
|
<td>File</td>
|
||||||
<td>
|
<td>
|
||||||
{{range $t := $item.Tags}}
|
{{range $t := $item.Tags}}
|
||||||
<code>{{$t}}</code>
|
<a href="/search?query=tags:{{$t}}" class="btn badge btn-primary">{{$t}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td><div class="btn-group">
|
<td><div class="btn-group">
|
||||||
|
|
@ -136,7 +140,7 @@ const (
|
||||||
<a href="#" class="btn btn-sm btn-warning">Scanning</a>
|
<a href="#" class="btn btn-sm btn-warning">Scanning</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{if .Data.Flag}}
|
{{if .Data.Flag}}
|
||||||
<a href="/reindex{{.Data.Name}}" class="btn btn-sm btn-primary">Reindex</a>
|
<a href="/retag{{.Data.Name}}" class="btn btn-sm btn-primary">Retag</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="/index{{.Data.Name}}" class="btn btn-sm btn-success">Index</a>
|
<a href="/index{{.Data.Name}}" class="btn btn-sm btn-success">Index</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -147,6 +151,16 @@ const (
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{if .Tags}}
|
||||||
|
<div class="row">
|
||||||
|
<div class="alert alert-secondary full-width">
|
||||||
|
Tags
|
||||||
|
{{range $t := .Tags}}
|
||||||
|
<a href="/search?query=tags:{{$t}}" class="btn badge btn-primary tag">{{$t}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<embed src="{{.Data.Abs}}" width="100%" height="700px" toolbar="1" statusbar="1" navpanes="1"></embed>
|
<embed src="{{.Data.Abs}}" width="100%" height="700px" toolbar="1" statusbar="1" navpanes="1"></embed>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -174,7 +188,7 @@ const (
|
||||||
<title>DocStore | {{.Title}}</title>
|
<title>DocStore | {{.Title}}</title>
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link>
|
<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="/bootstrap.css" media="screen" integrity="sha256-3YeveuySvBgp2GItUS8eV1R/9T1zcPMyvw22SyVf8qo="></link>
|
||||||
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-TjpqFXPvQzoakyE489hpNO1YK+wUAVd/AdwqpNz1gwc="></link>
|
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-6jxgK5/GbgoDJ3zQ2bcYXgSUsOvQ1IF18Krslbwlk7w="></link>
|
||||||
<meta name="theme-color" content="#375a7f">
|
<meta name="theme-color" content="#375a7f">
|
||||||
<meta name="description" content="{{.Title}}">
|
<meta name="description" content="{{.Title}}">
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -298,7 +312,7 @@ const (
|
||||||
{{if .Login}}
|
{{if .Login}}
|
||||||
<form class="form-inline" method="post" action="/search">
|
<form class="form-inline" method="post" action="/search">
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<input type="text" class="form-control mr-sm-2 menu" placeholder="Search" name="search" value="{{.Search}}" autocomplete="off">
|
<input type="text" class="form-control mr-sm-2 menu" placeholder="Search" name="query" value="{{.Query}}" autocomplete="off">
|
||||||
<button class="btn btn-sm btn-info" type="submit">Search</button>
|
<button class="btn btn-sm btn-info" type="submit">Search</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -357,7 +371,7 @@ const (
|
||||||
<input class="form-control input-sm" type="text" id="name" name="name" autocomplete="off" value="{{.Data}}" autofocus required>
|
<input class="form-control input-sm" type="text" id="name" name="name" autocomplete="off" value="{{.Data}}" autofocus required>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
<a href="{{.Path}}" class="btn btn-sm btn-primary">Back</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -422,7 +436,7 @@ const (
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control input-sm" placeholder="Search" name="search" value="{{.Search}}" autocomplete="off" autofocus>
|
<input type="text" class="form-control input-sm" placeholder="Query" name="query" value="{{.Query}}" autocomplete="off" autofocus>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Search</button>
|
<button class="btn btn-sm btn-primary" type="submit">Search</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -437,7 +451,7 @@ const (
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>File</th>
|
<th>File</th>
|
||||||
<th>Tags</th>
|
<th>Tag</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -449,7 +463,9 @@ const (
|
||||||
<br><pre class="wrap">{{$item.HTML}}</pre>
|
<br><pre class="wrap">{{$item.HTML}}</pre>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td>{{$item.Tags}}</td>
|
<td>
|
||||||
|
{{if $item.Tags}}<a href="/search?query=tags:{{$item.Tags}}" class="btn badge btn-primary">{{$item.Tags}}</a>{{end}}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -547,7 +563,7 @@ created:<="2016-09-21"</code></pre>
|
||||||
{{range $item := .Data}}
|
{{range $item := .Data}}
|
||||||
{{$len := len $item.Regex}}
|
{{$len := len $item.Regex}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{$item.Name}}</td>
|
<td><a href="/search?query=tags:{{$item.Name}}" class="btn badge btn-primary tag">{{$item.Name}}</a></td>
|
||||||
<td><code>{{printf "%.50s" $item.Regex}}{{if gt $len 50}}...{{end}}</code></td>
|
<td><code>{{printf "%.50s" $item.Regex}}{{if gt $len 50}}...{{end}}</code></td>
|
||||||
<td><div class="btn-group">
|
<td><div class="btn-group">
|
||||||
<a href="/tags/edit/{{$item.Name}}" class="btn btn-sm btn-primary">Edit</a>
|
<a href="/tags/edit/{{$item.Name}}" class="btn btn-sm btn-primary">Edit</a>
|
||||||
|
|
@ -728,6 +744,9 @@ body {
|
||||||
a.dark {
|
a.dark {
|
||||||
color: #222222;
|
color: #222222;
|
||||||
}
|
}
|
||||||
|
a.tag {
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
a.para {
|
a.para {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ body {
|
||||||
a.dark {
|
a.dark {
|
||||||
color: #222222;
|
color: #222222;
|
||||||
}
|
}
|
||||||
|
a.tag {
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
a.para {
|
a.para {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,21 @@
|
||||||
<li class="breadcrumb-item{{if $item.Flag}} active{{end}}">{{if $item.Flag}}{{$item.Name}}{{else}}<a href="{{$item.Abs}}">{{$item.Name}}</a>{{end}}</li>
|
<li class="breadcrumb-item{{if $item.Flag}} active{{end}}">{{if $item.Flag}}{{$item.Name}}{{else}}<a href="{{$item.Abs}}">{{$item.Name}}</a>{{end}}</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ol>
|
</ol>
|
||||||
<div class="btn-group btn-breadcrumb">
|
<form action="/index/all" method="post">
|
||||||
<a href="/upload{{.Path}}" class="btn btn-sm btn-success">Upload</a>
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<a href="/new{{.Path}}" class="btn btn-sm btn-primary">New Directory</a>
|
<div class="btn-group btn-breadcrumb">
|
||||||
{{$pl := len .Paths}}
|
<a href="/upload{{.Path}}" class="btn btn-sm btn-success">Upload</a>
|
||||||
{{if eq $pl 0}}
|
<a href="/new{{.Path}}" class="btn btn-sm btn-primary">New Directory</a>
|
||||||
<a href="/reindex" class="btn btn-sm btn-warning">Reindex All</a>
|
{{$pl := len .Paths}}
|
||||||
{{else}}
|
{{if eq $pl 0}}
|
||||||
<a href="/move{{.Path}}" class="btn btn-sm btn-primary">Move</a>
|
<a href="/retag" class="btn btn-sm btn-warning">Retag All</a>
|
||||||
<a href="/delete{{.Path}}" class="btn btn-sm btn-danger">Delete</a>
|
<button class="btn btn-sm btn-warning" type="submit">Index All</button>
|
||||||
{{end}}
|
{{else}}
|
||||||
</div>
|
<a href="/move{{.Path}}" class="btn btn-sm btn-primary">Move</a>
|
||||||
|
<a href="/delete{{.Path}}" class="btn btn-sm btn-danger">Delete</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
@ -50,7 +54,7 @@
|
||||||
<td>File</td>
|
<td>File</td>
|
||||||
<td>
|
<td>
|
||||||
{{range $t := $item.Tags}}
|
{{range $t := $item.Tags}}
|
||||||
<code>{{$t}}</code>
|
<a href="/search?query=tags:{{$t}}" class="btn badge btn-primary">{{$t}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td><div class="btn-group">
|
<td><div class="btn-group">
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<a href="#" class="btn btn-sm btn-warning">Scanning</a>
|
<a href="#" class="btn btn-sm btn-warning">Scanning</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{if .Data.Flag}}
|
{{if .Data.Flag}}
|
||||||
<a href="/reindex{{.Data.Name}}" class="btn btn-sm btn-primary">Reindex</a>
|
<a href="/retag{{.Data.Name}}" class="btn btn-sm btn-primary">Retag</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="/index{{.Data.Name}}" class="btn btn-sm btn-success">Index</a>
|
<a href="/index{{.Data.Name}}" class="btn btn-sm btn-success">Index</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -24,6 +24,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{if .Tags}}
|
||||||
|
<div class="row">
|
||||||
|
<div class="alert alert-secondary full-width">
|
||||||
|
Tags
|
||||||
|
{{range $t := .Tags}}
|
||||||
|
<a href="/search?query=tags:{{$t}}" class="btn badge btn-primary tag">{{$t}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<embed src="{{.Data.Abs}}" width="100%" height="700px" toolbar="1" statusbar="1" navpanes="1"></embed>
|
<embed src="{{.Data.Abs}}" width="100%" height="700px" toolbar="1" statusbar="1" navpanes="1"></embed>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<title>DocStore | {{.Title}}</title>
|
<title>DocStore | {{.Title}}</title>
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link>
|
<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="/bootstrap.css" media="screen" integrity="sha256-3YeveuySvBgp2GItUS8eV1R/9T1zcPMyvw22SyVf8qo="></link>
|
||||||
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-TjpqFXPvQzoakyE489hpNO1YK+wUAVd/AdwqpNz1gwc="></link>
|
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-6jxgK5/GbgoDJ3zQ2bcYXgSUsOvQ1IF18Krslbwlk7w="></link>
|
||||||
<meta name="theme-color" content="#375a7f">
|
<meta name="theme-color" content="#375a7f">
|
||||||
<meta name="description" content="{{.Title}}">
|
<meta name="description" content="{{.Title}}">
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
{{if .Login}}
|
{{if .Login}}
|
||||||
<form class="form-inline" method="post" action="/search">
|
<form class="form-inline" method="post" action="/search">
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<input type="text" class="form-control mr-sm-2 menu" placeholder="Search" name="search" value="{{.Search}}" autocomplete="off">
|
<input type="text" class="form-control mr-sm-2 menu" placeholder="Search" name="query" value="{{.Query}}" autocomplete="off">
|
||||||
<button class="btn btn-sm btn-info" type="submit">Search</button>
|
<button class="btn btn-sm btn-info" type="submit">Search</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<input class="form-control input-sm" type="text" id="name" name="name" autocomplete="off" value="{{.Data}}" autofocus required>
|
<input class="form-control input-sm" type="text" id="name" name="name" autocomplete="off" value="{{.Data}}" autofocus required>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
<a href="{{.Path}}" class="btn btn-sm btn-primary">Back</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control input-sm" placeholder="Search" name="search" value="{{.Search}}" autocomplete="off" autofocus>
|
<input type="text" class="form-control input-sm" placeholder="Query" name="query" value="{{.Query}}" autocomplete="off" autofocus>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Search</button>
|
<button class="btn btn-sm btn-primary" type="submit">Search</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>File</th>
|
<th>File</th>
|
||||||
<th>Tags</th>
|
<th>Tag</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -37,7 +37,9 @@
|
||||||
<br><pre class="wrap">{{$item.HTML}}</pre>
|
<br><pre class="wrap">{{$item.HTML}}</pre>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td>{{$item.Tags}}</td>
|
<td>
|
||||||
|
{{if $item.Tags}}<a href="/search?query=tags:{{$item.Tags}}" class="btn badge btn-primary">{{$item.Tags}}</a>{{end}}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
{{range $item := .Data}}
|
{{range $item := .Data}}
|
||||||
{{$len := len $item.Regex}}
|
{{$len := len $item.Regex}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{$item.Name}}</td>
|
<td><a href="/search?query=tags:{{$item.Name}}" class="btn badge btn-primary tag">{{$item.Name}}</a></td>
|
||||||
<td><code>{{printf "%.50s" $item.Regex}}{{if gt $len 50}}...{{end}}</code></td>
|
<td><code>{{printf "%.50s" $item.Regex}}{{if gt $len 50}}...{{end}}</code></td>
|
||||||
<td><div class="btn-group">
|
<td><div class="btn-group">
|
||||||
<a href="/tags/edit/{{$item.Name}}" class="btn btn-sm btn-primary">Edit</a>
|
<a href="/tags/edit/{{$item.Name}}" class="btn btn-sm btn-primary">Edit</a>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue