some bug fixes

This commit is contained in:
ston1th 2019-05-05 19:06:51 +02:00
commit 0e914c629e
16 changed files with 179 additions and 52 deletions

View file

@ -4,9 +4,11 @@ package cmd
import ( import (
"encoding/json" "encoding/json"
"errors"
"git.giftfish.de/ston1th/docstore/pkg/core" "git.giftfish.de/ston1th/docstore/pkg/core"
"git.giftfish.de/ston1th/docstore/pkg/db" "git.giftfish.de/ston1th/docstore/pkg/db"
"git.giftfish.de/ston1th/docstore/pkg/log" "git.giftfish.de/ston1th/docstore/pkg/log"
"git.giftfish.de/ston1th/docstore/pkg/scan"
"git.giftfish.de/ston1th/docstore/pkg/server" "git.giftfish.de/ston1th/docstore/pkg/server"
"git.giftfish.de/ston1th/godrop/v2" "git.giftfish.de/ston1th/godrop/v2"
"github.com/urfave/cli" "github.com/urfave/cli"
@ -26,6 +28,8 @@ const (
defRunUser = "docstore" defRunUser = "docstore"
defRunGroup = "docstore" defRunGroup = "docstore"
defTimeout = 600
) )
var ( var (
@ -55,7 +59,7 @@ func initServer(cfg core.Config) (err error) {
return return
} }
if err = godrop.PledgePromises("stdio rpath wpath cpath inet fattr flock unveil"); err != nil { if err = godrop.PledgePromises("stdio rpath wpath cpath inet fattr flock proc exec unveil"); err != nil {
return return
} }
@ -64,6 +68,14 @@ func initServer(cfg core.Config) (err error) {
return return
} }
log.InitLogger(cfg)
cfg.DataDir = filepath.Join(cfg.RunDir, core.DataDir)
scanner, err := scan.New(cfg.DataDir, cfg.Langs, time.Second*time.Duration(cfg.CMDTimeout))
if err != nil {
return errors.New("scanner: " + err.Error())
}
err = godrop.Unveil(cfg.RunDir, "rwc") err = godrop.Unveil(cfg.RunDir, "rwc")
if err != nil { if err != nil {
return return
@ -73,10 +85,7 @@ func initServer(cfg core.Config) (err error) {
return return
} }
log.InitLogger(cfg) srv := server.NewHTTPServer(cfg, l, scanner)
cfg.DataDir = filepath.Join(cfg.RunDir, core.DataDir)
srv := server.NewHTTPServer(cfg, l)
err = srv.Start() err = srv.Start()
if err != nil { if err != nil {
return return
@ -124,6 +133,7 @@ func Run(version string) {
Usage: "start docstore server", Usage: "start docstore server",
Flags: defaultFlags(serverFlags()), Flags: defaultFlags(serverFlags()),
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
config.Langs = c.StringSlice("langs")
if err := loadConfig(); err != nil { if err := loadConfig(); err != nil {
stdlog.Fatal("server: ", err) stdlog.Fatal("server: ", err)
} }
@ -235,6 +245,16 @@ func serverFlags() []cli.Flag {
Usage: "static hmac secret (at least 64 chars, used for JWT signing)", Usage: "static hmac secret (at least 64 chars, used for JWT signing)",
Destination: &config.Secret, Destination: &config.Secret,
}, },
cli.StringSliceFlag{
Name: "langs",
Usage: "languages for tesseract ocr",
},
cli.IntFlag{
Name: "timeout, t",
Value: defTimeout,
Usage: "timeout in seconds for executed teseract and pdftotext commands",
Destination: &config.CMDTimeout,
},
cli.BoolFlag{ cli.BoolFlag{
Name: "foreground, f", Name: "foreground, f",
Usage: "do not fork into the background", Usage: "do not fork into the background",

View file

@ -3,6 +3,7 @@
package core package core
import ( import (
"sync"
"time" "time"
) )
@ -10,6 +11,13 @@ const (
fileFmt = "20060102_150405" fileFmt = "20060102_150405"
) )
var m sync.Mutex
func FileName() string { func FileName() string {
m.Lock()
defer func() {
time.Sleep(time.Millisecond * 1100)
m.Unlock()
}()
return time.Now().Format(fileFmt) return time.Now().Format(fileFmt)
} }

View file

@ -24,6 +24,7 @@ type Path struct {
Name string Name string
Abs string Abs string
Flag bool Flag bool
Scan bool
} }
type Paths []Path type Paths []Path
@ -41,6 +42,8 @@ type Config struct {
LogFile string `json:"log_file,omitempty"` LogFile string `json:"log_file,omitempty"`
Version string `json:"-"` Version string `json:"-"`
Secret string `json:"secret,omitempty"` Secret string `json:"secret,omitempty"`
Langs []string `json:"langs,omitempty"`
CMDTimeout int `json:"cmd_timeout,omitempty"`
Foreground bool `json:"foreground,omitempty"` Foreground bool `json:"foreground,omitempty"`
SecureCookie bool `json:"secure_cookie,omitempty"` SecureCookie bool `json:"secure_cookie,omitempty"`
Debug bool `json:"debug,omitempty"` Debug bool `json:"debug,omitempty"`

View file

@ -7,6 +7,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
) )
type Mode int type Mode int
@ -78,20 +79,40 @@ func (fs *Filesystem) Mkdir(path, name string) error {
return os.Mkdir(filepath.Join(fs.Base, dirpath), DirMode) return os.Mkdir(filepath.Join(fs.Base, dirpath), DirMode)
} }
func (fs *Filesystem) Move(path, dest, name string) (string, error) { type Moved struct {
Old string
New string
}
func (fs *Filesystem) Move(path, dest, name string) (moved []Moved, newfile string, err error) {
if name == "" { if name == "" {
name = File(path) name = File(path)
} }
fullpath := filepath.Join(fs.Base, Clean(path)) path = Clean(path)
fullpath := filepath.Join(fs.Base, path)
if fullpath == fs.Base { if fullpath == fs.Base {
return "", errors.New("the root path can not be moved") err = errors.New("the root path can not be moved")
return
} }
newfile := filepath.Join(Clean(dest), Clean(name)) newfile = filepath.Join(Clean(dest), Clean(name))
err := checkPath(newfile) err = checkPath(newfile)
if err != nil { if err != nil {
return "", err return
} }
return newfile, os.Rename(fullpath, filepath.Join(fs.Base, newfile)) fi, err := os.Stat(fullpath)
if err != nil {
return
}
m := fi.Mode()
if m.IsDir() {
for _, f := range fs.RecursiveFiles(path) {
moved = append(moved, Moved{f, strings.Replace(f, path, newfile, 1)})
}
} else if m.IsRegular() {
moved = append(moved, Moved{path, newfile})
}
err = os.Rename(fullpath, filepath.Join(fs.Base, newfile))
return
} }
func (fs *Filesystem) Delete(path string) (removed []string, err error) { func (fs *Filesystem) Delete(path string) (removed []string, err error) {

View file

@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"sync"
) )
const ( const (
@ -25,6 +26,10 @@ type Directory struct {
type Filesystem struct { type Filesystem struct {
Base string Base string
// protects scan
m sync.RWMutex
scan map[string]struct{}
} }
func NewFilesystem(base string) (fs *Filesystem, err error) { func NewFilesystem(base string) (fs *Filesystem, err error) {
@ -48,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} fs = &Filesystem{Base: base, scan: make(map[string]struct{})}
return return
} }
@ -75,7 +80,8 @@ func (fs *Filesystem) ReadDir(path string) (d *Directory) {
continue continue
} }
if m.IsRegular() { if m.IsRegular() {
d.Files = append(d.Files, core.Path{Name: n, Abs: filepath.Join(path, n)}) abs := filepath.Join(path, n)
d.Files = append(d.Files, core.Path{Name: n, Abs: abs, Scan: fs.CheckScan(abs)})
} }
} }
sort.Sort(d.Dirs) sort.Sort(d.Dirs)

26
pkg/fs/scan.go Normal file
View file

@ -0,0 +1,26 @@
package fs
import "errors"
func (fs *Filesystem) AddScan(path string) error {
if fs.CheckScan(path) {
return errors.New("file '" + path + "' is already scanning")
}
fs.m.Lock()
defer fs.m.Unlock()
fs.scan[path] = struct{}{}
return nil
}
func (fs *Filesystem) CheckScan(path string) (ok bool) {
fs.m.RLock()
defer fs.m.RUnlock()
_, ok = fs.scan[path]
return
}
func (fs *Filesystem) RemoveScan(path string) {
fs.m.Lock()
defer fs.m.Unlock()
delete(fs.scan, path)
}

7
pkg/scan/args.go Normal file
View file

@ -0,0 +1,7 @@
// Copyright (C) 2019 Marius Schellenberger
// +build !openbsd
package scan
const tesseract_oem = "2"

7
pkg/scan/args_openbsd.go Normal file
View file

@ -0,0 +1,7 @@
// Copyright (C) 2019 Marius Schellenberger
// +build openbsd
package scan
const tesseract_oem = "1"

View file

@ -56,11 +56,11 @@ func New(base string, langs []string, timeout time.Duration) (s *Scanner, err er
if err != nil { if err != nil {
return return
} }
err = godrop.Unveil(tcmd, "rx") pcmd, err = exec.LookPath("pdftotext")
if err != nil { if err != nil {
return return
} }
pcmd, err = exec.LookPath("pdftotext") err = godrop.Unveil(tcmd, "rx")
if err != nil { if err != nil {
return return
} }
@ -73,11 +73,11 @@ func New(base string, langs []string, timeout time.Duration) (s *Scanner, err er
} }
for i, l := range langs { for i, l := range langs {
lang += l lang += l
if i < len(langs) { if i < len(langs)-1 {
lang += "+" lang += "+"
} }
} }
args := []string{"-l", lang, "--oem", "2"} args := []string{"-l", lang, "--oem", tesseract_oem}
s = &Scanner{ s = &Scanner{
base, base,
timeout, timeout,
@ -98,14 +98,14 @@ func (s *Scanner) Scan(file string) (filename, text string, err error) {
f *os.File f *os.File
) )
p, _ := filepath.Split(file) p, _ := filepath.Split(file)
filename = filepath.Join(p, core.FileName()+pdfExt)
pdfCmd := exec.Command(s.t.cmd, append(s.t.args, []string{scanfile, "stdout", "pdf"}...)...) pdfCmd := exec.Command(s.t.cmd, append(s.t.args, []string{scanfile, "stdout", "pdf"}...)...)
pdfCmd.Env = s.t.env pdfCmd.Env = s.t.env
log.Debugf("%v %s %v", pdfCmd.Env, pdfCmd.Path, pdfCmd.Args) log.Debugf("%v %s %v", pdfCmd.Env, pdfCmd.Path, pdfCmd.Args)
pdf, err = core.Timeout(pdfCmd, s.timeout) pdf, err = timeout(pdfCmd, s.timeout)
if err != nil { if err != nil {
return return
} }
filename = filepath.Join(p, core.FileName()+pdfExt)
pdffile = filepath.Join(s.base, filename) pdffile = filepath.Join(s.base, filename)
f, err = os.OpenFile(pdffile, os.O_RDWR|os.O_CREATE, fs.FileMode) f, err = os.OpenFile(pdffile, os.O_RDWR|os.O_CREATE, fs.FileMode)
if err != nil { if err != nil {
@ -124,8 +124,9 @@ func (s *Scanner) Scan(file string) (filename, text string, err error) {
} }
txtCmd := exec.Command(s.p.cmd, append(s.p.args, []string{pdffile, "-"}...)...) txtCmd := exec.Command(s.p.cmd, append(s.p.args, []string{pdffile, "-"}...)...)
log.Debugf("%s %v", txtCmd.Path, txtCmd.Args) log.Debugf("%s %v", txtCmd.Path, txtCmd.Args)
txt, err := core.Timeout(txtCmd, s.timeout) txt, err := timeout(txtCmd, s.timeout)
if err != nil { if err != nil {
log.Debugf("killed", err)
return return
} }
if len(txt) <= 2 { if len(txt) <= 2 {

View file

@ -1,14 +1,14 @@
// Copyright (C) 2019 Marius Schellenberger // Copyright (C) 2019 Marius Schellenberger
package core package scan
import ( import (
"errors" "fmt"
"os/exec" "os/exec"
"time" "time"
) )
func Timeout(cmd *exec.Cmd, t time.Duration) (output []byte, err error) { func timeout(cmd *exec.Cmd, t time.Duration) (output []byte, err error) {
done := make(chan error) done := make(chan error)
out := make(chan []byte) out := make(chan []byte)
go func() { go func() {
@ -19,17 +19,17 @@ func Timeout(cmd *exec.Cmd, t time.Duration) (output []byte, err error) {
select { select {
case <-time.After(t): case <-time.After(t):
e := cmd.Process.Kill() e := cmd.Process.Kill()
err = <-done
<-out <-out
if e != nil { if e != nil {
<-done
err = e err = e
return } else {
err = fmt.Errorf("command '%s' timed out: %s", cmd.Path, err)
} }
err = <-done
case e := <-done: case e := <-done:
exerr, ok := e.(*exec.ExitError) exerr, ok := e.(*exec.ExitError)
if ok { if ok {
err = errors.New(e.Error() + "\n" + string(exerr.Stderr)) err = fmt.Errorf("%s\n%s", e, string(exerr.Stderr))
} else { } else {
err = e err = e
} }

View file

@ -179,11 +179,22 @@ func indexHandler(ctx *Context) {
case "GET": case "GET":
earlyErr := make(chan error) earlyErr := make(chan error)
go func() { go func() {
err := ctx.Srv.FS.AddScan(path)
if err != nil {
select {
case earlyErr <- err:
default:
}
return
}
defer ctx.Srv.FS.RemoveScan(path)
file, txt, err := ctx.Srv.Scanner.Scan(path) file, txt, err := ctx.Srv.Scanner.Scan(path)
if err != nil { if err != nil {
select { select {
case earlyErr <- err: case earlyErr <- err:
default:
} }
log.Printf("scanner: %s: %s", path, err)
return return
} }
id, err := ctx.Srv.DB.Index.Add(txt) id, err := ctx.Srv.DB.Index.Add(txt)
@ -306,15 +317,17 @@ func moveHandler(ctx *Context) {
} }
name := ctx.Form("name") name := ctx.Form("name")
dest := ctx.Form("destination") dest := ctx.Form("destination")
newfile, err := ctx.Srv.FS.Move(path, dest, name) moved, newfile, err := ctx.Srv.FS.Move(path, dest, name)
if err != nil { if err != nil {
ctx.Error(err) ctx.Error(err)
return return
} }
err = ctx.Srv.DB.MoveFile(path, newfile) for _, m := range moved {
log.Debugf("move: %s to %s", m.Old, m.New)
err = ctx.Srv.DB.MoveFile(m.Old, m.New)
if err != nil { if err != nil {
ctx.Error(err) log.Printf("move: %s: %s", m, err)
return }
} }
ctx.Redirect(newfile, http.StatusFound) ctx.Redirect(newfile, http.StatusFound)
} }
@ -411,6 +424,7 @@ func dirHandler(ctx *Context) {
Name: path, Name: path,
Abs: core.RawPrefix + path, Abs: core.RawPrefix + path,
Flag: ctx.Srv.DB.IsIndexed(path), Flag: ctx.Srv.DB.IsIndexed(path),
Scan: ctx.Srv.FS.CheckScan(path),
} }
ctx.Data.Paths = fs.Paths(path) ctx.Data.Paths = fs.Paths(path)
ctx.Exec() ctx.Exec()

View file

@ -40,10 +40,11 @@ type HTTPServer struct {
res map[string][]byte res map[string][]byte
} }
func NewHTTPServer(cfg core.Config, l net.Listener) (srv *HTTPServer) { func NewHTTPServer(cfg core.Config, l net.Listener, s *scan.Scanner) (srv *HTTPServer) {
srv = &HTTPServer{ srv = &HTTPServer{
Config: cfg, Config: cfg,
listener: l, listener: l,
Scanner: s,
} }
srv.handler = srv.buildRoutes() srv.handler = srv.buildRoutes()
srv.loadTemplates() srv.loadTemplates()
@ -56,10 +57,6 @@ func (s *HTTPServer) Start() (err error) {
if err != nil { if err != nil {
return errors.New("fs: " + err.Error()) return errors.New("fs: " + err.Error())
} }
s.Scanner, err = scan.New(s.Config.DataDir, nil, time.Second*30)
if err != nil {
return errors.New("tesseract: " + err.Error())
}
s.DB, err = db.New(s.Config) s.DB, err = db.New(s.Config)
if err != nil { if err != nil {
return errors.New("db: " + err.Error()) return errors.New("db: " + err.Error())

View file

@ -67,7 +67,7 @@ const (
{{if .Data}} {{if .Data}}
{{range $item := .Data.Dirs}} {{range $item := .Data.Dirs}}
<tr> <tr>
<th><a href="{{$item.Abs}}">{{$item.Name}}/</a></th> <th><a href="{{$item.Abs}}">{{$item.Name}}</a></th>
<td>Directory</td> <td>Directory</td>
<td><div class="btn-group"> <td><div class="btn-group">
<a href="/move{{$item.Abs}}" class="btn btn-sm btn-primary">Move</a> <a href="/move{{$item.Abs}}" class="btn btn-sm btn-primary">Move</a>
@ -80,9 +80,13 @@ const (
<th><a href="{{$item.Abs}}">{{$item.Name}}</a></th> <th><a href="{{$item.Abs}}">{{$item.Name}}</a></th>
<td>File</td> <td>File</td>
<td><div class="btn-group"> <td><div class="btn-group">
{{if $item.Scan}}
<a href="#" class="btn btn-sm btn-warning">Scanning..</a>
{{else}}
{{if not $item.Flag}} {{if not $item.Flag}}
<a href="/index{{$item.Abs}}" class="btn btn-sm btn-success">Index</a> <a href="/index{{$item.Abs}}" class="btn btn-sm btn-success">Index</a>
{{end}} {{end}}
{{end}}
<a href="/move{{$item.Abs}}" class="btn btn-sm btn-primary">Move</a> <a href="/move{{$item.Abs}}" class="btn btn-sm btn-primary">Move</a>
<a href="/delete{{$item.Abs}}" class="btn btn-sm btn-danger">Delete</a> <a href="/delete{{$item.Abs}}" class="btn btn-sm btn-danger">Delete</a>
</div></td> </div></td>
@ -104,9 +108,13 @@ const (
{{end}} {{end}}
</ol> </ol>
<div class="btn-group btn-breadcrumb"> <div class="btn-group btn-breadcrumb">
{{if .Data.Scan}}
<a href="#" class="btn btn-sm btn-warning">Scanning..</a>
{{else}}
{{if not .Data.Flag}} {{if not .Data.Flag}}
<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}}
{{end}}
<a href="/move{{.Data.Name}}" class="btn btn-sm btn-primary">Move</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> <a href="/delete{{.Data.Name}}" class="btn btn-sm btn-danger">Delete</a>
</div> </div>

View file

@ -1,6 +1,7 @@
#!/bin/sh #!/bin/sh
pkg_add poppler tesseract tesseract-deu tesseract-eng # requires the xbase and xfont sets
pkg_add poppler-utils tesseract tesseract-deu tesseract-eng
# rc script # rc script
cat <<'EOF'> /etc/rc.d/docstore cat <<'EOF'> /etc/rc.d/docstore

View file

@ -29,7 +29,7 @@
{{if .Data}} {{if .Data}}
{{range $item := .Data.Dirs}} {{range $item := .Data.Dirs}}
<tr> <tr>
<th><a href="{{$item.Abs}}">{{$item.Name}}/</a></th> <th><a href="{{$item.Abs}}">{{$item.Name}}</a></th>
<td>Directory</td> <td>Directory</td>
<td><div class="btn-group"> <td><div class="btn-group">
<a href="/move{{$item.Abs}}" class="btn btn-sm btn-primary">Move</a> <a href="/move{{$item.Abs}}" class="btn btn-sm btn-primary">Move</a>
@ -42,9 +42,13 @@
<th><a href="{{$item.Abs}}">{{$item.Name}}</a></th> <th><a href="{{$item.Abs}}">{{$item.Name}}</a></th>
<td>File</td> <td>File</td>
<td><div class="btn-group"> <td><div class="btn-group">
{{if $item.Scan}}
<a href="#" class="btn btn-sm btn-warning">Scanning..</a>
{{else}}
{{if not $item.Flag}} {{if not $item.Flag}}
<a href="/index{{$item.Abs}}" class="btn btn-sm btn-success">Index</a> <a href="/index{{$item.Abs}}" class="btn btn-sm btn-success">Index</a>
{{end}} {{end}}
{{end}}
<a href="/move{{$item.Abs}}" class="btn btn-sm btn-primary">Move</a> <a href="/move{{$item.Abs}}" class="btn btn-sm btn-primary">Move</a>
<a href="/delete{{$item.Abs}}" class="btn btn-sm btn-danger">Delete</a> <a href="/delete{{$item.Abs}}" class="btn btn-sm btn-danger">Delete</a>
</div></td> </div></td>

View file

@ -9,9 +9,13 @@
{{end}} {{end}}
</ol> </ol>
<div class="btn-group btn-breadcrumb"> <div class="btn-group btn-breadcrumb">
{{if .Data.Scan}}
<a href="#" class="btn btn-sm btn-warning">Scanning..</a>
{{else}}
{{if not .Data.Flag}} {{if not .Data.Flag}}
<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}}
{{end}}
<a href="/move{{.Data.Name}}" class="btn btn-sm btn-primary">Move</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> <a href="/delete{{.Data.Name}}" class="btn btn-sm btn-danger">Delete</a>
</div> </div>