some improvements

This commit is contained in:
ston1th 2019-08-26 19:58:31 +02:00
commit 58a0dc36d5
25 changed files with 233 additions and 105 deletions

View file

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

View file

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

View file

@ -14,7 +14,7 @@ const (
RawPrefix = "/raw" RawPrefix = "/raw"
IndexPrefix = "/index" IndexPrefix = "/index"
ReindexPrefix = "/reindex" RetagPrefix = "/retag"
UploadPrefix = "/upload" UploadPrefix = "/upload"
NewPrefix = "/new" NewPrefix = "/new"
MovePrefix = "/move" MovePrefix = "/move"

View file

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

View file

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

View file

@ -27,7 +27,7 @@ var migrators = []migrator{
if err != nil { if err != nil {
return return
} }
return db.ReindexAll() return db.RetagAll()
}, },
} }

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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,

View file

@ -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)
} }

View file

@ -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"
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 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
} }

View file

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

View file

@ -72,10 +72,11 @@ type webData struct {
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

View file

@ -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()
} }
} }

View file

@ -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)),

View file

@ -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()

View file

@ -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>
<form action="/index/all" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="btn-group btn-breadcrumb"> <div class="btn-group btn-breadcrumb">
<a href="/upload{{.Path}}" class="btn btn-sm btn-success">Upload</a> <a href="/upload{{.Path}}" class="btn btn-sm btn-success">Upload</a>
<a href="/new{{.Path}}" class="btn btn-sm btn-primary">New Directory</a> <a href="/new{{.Path}}" class="btn btn-sm btn-primary">New Directory</a>
{{$pl := len .Paths}} {{$pl := len .Paths}}
{{if eq $pl 0}} {{if eq $pl 0}}
<a href="/reindex" class="btn btn-sm btn-warning">Reindex All</a> <a href="/retag" class="btn btn-sm btn-warning">Retag All</a>
<button class="btn btn-sm btn-warning" type="submit">Index All</button>
{{else}} {{else}}
<a href="/move{{.Path}}" class="btn btn-sm btn-primary">Move</a> <a href="/move{{.Path}}" class="btn btn-sm btn-primary">Move</a>
<a href="/delete{{.Path}}" class="btn btn-sm btn-danger">Delete</a> <a href="/delete{{.Path}}" class="btn btn-sm btn-danger">Delete</a>
{{end}} {{end}}
</div> </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:&lt;="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;

View file

@ -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;

View file

@ -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>
<form action="/index/all" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="btn-group btn-breadcrumb"> <div class="btn-group btn-breadcrumb">
<a href="/upload{{.Path}}" class="btn btn-sm btn-success">Upload</a> <a href="/upload{{.Path}}" class="btn btn-sm btn-success">Upload</a>
<a href="/new{{.Path}}" class="btn btn-sm btn-primary">New Directory</a> <a href="/new{{.Path}}" class="btn btn-sm btn-primary">New Directory</a>
{{$pl := len .Paths}} {{$pl := len .Paths}}
{{if eq $pl 0}} {{if eq $pl 0}}
<a href="/reindex" class="btn btn-sm btn-warning">Reindex All</a> <a href="/retag" class="btn btn-sm btn-warning">Retag All</a>
<button class="btn btn-sm btn-warning" type="submit">Index All</button>
{{else}} {{else}}
<a href="/move{{.Path}}" class="btn btn-sm btn-primary">Move</a> <a href="/move{{.Path}}" class="btn btn-sm btn-primary">Move</a>
<a href="/delete{{.Path}}" class="btn btn-sm btn-danger">Delete</a> <a href="/delete{{.Path}}" class="btn btn-sm btn-danger">Delete</a>
{{end}} {{end}}
</div> </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">

View file

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

View file

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

View file

@ -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}}

View file

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

View file

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

View file

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