diff --git a/.gitignore b/.gitignore
index a4694b8..2f4283f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-gowiki
+docstore
tmp
run.sh
TODO.txt
diff --git a/README.md b/README.md
index eb96770..a15d39d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# GoWiki - a simple wiki engine written in Go
+# DocStore - document server and indexing engine
## Building
@@ -24,4 +24,4 @@ GOOS=openbsd make vendor
## Install
-See the `scripts/` directory on how to install gowiki on Linux or OpenBSD.
+See the `scripts/` directory on how to install docstore on OpenBSD.
diff --git a/docstore b/docstore
deleted file mode 100755
index 981b967..0000000
Binary files a/docstore and /dev/null differ
diff --git a/pkg/db/index.go b/pkg/db/index.go
index 343e5f2..ec54219 100644
--- a/pkg/db/index.go
+++ b/pkg/db/index.go
@@ -67,6 +67,9 @@ func (db *DB) MoveFile(op, np string) error {
}
func (db *DB) DeleteFile(path string) (id string, err error) {
+ if !db.IsIndexed(path) {
+ return
+ }
id, err = db.GetID(path)
if err != nil {
return
diff --git a/pkg/fs/file.go b/pkg/fs/file.go
index dce9997..18584b8 100644
--- a/pkg/fs/file.go
+++ b/pkg/fs/file.go
@@ -56,7 +56,7 @@ func (fs *Filesystem) Delete(path string) (removed []string, err error) {
if fullpath == fs.Base {
return nil, errors.New("the root path can not be deleted")
}
- fi, err := os.Stat(path)
+ fi, err := os.Stat(fullpath)
if err != nil {
return
}
diff --git a/pkg/fs/fs.go b/pkg/fs/fs.go
index ab90599..193d145 100644
--- a/pkg/fs/fs.go
+++ b/pkg/fs/fs.go
@@ -116,5 +116,8 @@ func Paths(path string) (p core.Paths) {
}
p = append(p, core.Path{Name: v, Abs: filepath.Join(p[i-1].Abs, v)})
}
+ if len(p) != 0 {
+ p[len(p)-1].Indexed = true
+ }
return
}
diff --git a/pkg/scan/helper.go b/pkg/scan/helper.go
new file mode 100644
index 0000000..7567d0f
--- /dev/null
+++ b/pkg/scan/helper.go
@@ -0,0 +1,10 @@
+package scan
+
+import (
+ "path/filepath"
+ "strings"
+)
+
+func isPDF(path string) bool {
+ return strings.ToLower(filepath.Ext(path)) == pdfExt
+}
diff --git a/pkg/scan/pdf.go b/pkg/scan/pdf.go
new file mode 100644
index 0000000..6fe939f
--- /dev/null
+++ b/pkg/scan/pdf.go
@@ -0,0 +1,6 @@
+package scan
+
+type PDF struct {
+ cmd string
+ args []string
+}
diff --git a/pkg/scan/scanner.go b/pkg/scan/scanner.go
new file mode 100644
index 0000000..899fe26
--- /dev/null
+++ b/pkg/scan/scanner.go
@@ -0,0 +1,124 @@
+package scan
+
+import (
+ "errors"
+ "fmt"
+ "git.giftfish.de/ston1th/docstore/pkg/core"
+ "git.giftfish.de/ston1th/docstore/pkg/log"
+ "git.giftfish.de/ston1th/godrop/v2"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "time"
+)
+
+const pdfExt = ".pdf"
+
+var (
+ wsr = regexp.MustCompile(`[\t\f ]+`)
+ nlr = regexp.MustCompile(`[\n]{3,}`)
+ ws = []byte(" ")
+ nl = []byte("\n")
+)
+
+type Scanner struct {
+ base string
+ timeout time.Duration
+ t Tesseract
+ p PDF
+}
+
+func New(base string, langs []string, timeout time.Duration) (s *Scanner, err error) {
+ var (
+ tcmd string
+ pcmd string
+ lang string
+ )
+ tcmd, err = exec.LookPath("tesseract")
+ if err != nil {
+ return
+ }
+ err = godrop.Unveil(tcmd, "rx")
+ if err != nil {
+ return
+ }
+ pcmd, err = exec.LookPath("pdftotext")
+ if err != nil {
+ return
+ }
+ err = godrop.Unveil(pcmd, "rx")
+ if err != nil {
+ return
+ }
+ if langs == nil {
+ langs = defaultLangs
+ }
+ for i, l := range langs {
+ lang += l
+ if i < len(langs) {
+ lang += "+"
+ }
+ }
+ args := []string{"-l", lang, "--oem", "2"}
+ s = &Scanner{
+ base,
+ timeout,
+ Tesseract{tcmd, args, []string{fmt.Sprintf("OMP_THREAD_LIMIT=%d", runtime.NumCPU())}},
+ PDF{pcmd, []string{"-layout", "-nopgbrk", "-eol", "unix"}},
+ }
+ return
+}
+
+func (s *Scanner) Scan(file string) (filename, text string, err error) {
+ file = filepath.Clean(file)
+ scanfile := filepath.Join(s.base, file)
+ var pdffile string
+ pdf := isPDF(scanfile)
+ if !pdf {
+ var (
+ pdf []byte
+ f *os.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.Env = s.t.env
+ log.Debugf("%v %s %v", pdfCmd.Env, pdfCmd.Path, pdfCmd.Args)
+ pdf, err = core.Timeout(pdfCmd, s.timeout)
+ if err != nil {
+ return
+ }
+ pdffile = filepath.Join(s.base, filename)
+ f, err = os.OpenFile(pdffile, os.O_RDWR|os.O_CREATE, 0640)
+ if err != nil {
+ f.Close()
+ return
+ }
+ _, err = f.Write(pdf)
+ if err != nil {
+ f.Close()
+ return
+ }
+ f.Close()
+ } else {
+ pdffile = scanfile
+ filename = file
+ }
+ txtCmd := exec.Command(s.p.cmd, append(s.p.args, []string{pdffile, "-"}...)...)
+ log.Debugf("%s %v", txtCmd.Path, txtCmd.Args)
+ txt, err := core.Timeout(txtCmd, s.timeout)
+ if err != nil {
+ return
+ }
+ if len(txt) <= 2 {
+ err = errors.New("pdf does not contain any text information")
+ return
+ }
+ text = string(nlr.ReplaceAll(wsr.ReplaceAll(txt, ws), nl))
+ if !pdf {
+ err = os.Remove(scanfile)
+ }
+ return
+}
diff --git a/pkg/scan/tesseract.go b/pkg/scan/tesseract.go
new file mode 100644
index 0000000..fc667cf
--- /dev/null
+++ b/pkg/scan/tesseract.go
@@ -0,0 +1,9 @@
+package scan
+
+var defaultLangs = []string{"eng", "deu"}
+
+type Tesseract struct {
+ cmd string
+ args []string
+ env []string
+}
diff --git a/pkg/server/handler.go b/pkg/server/handler.go
index ef3dd53..9883170 100644
--- a/pkg/server/handler.go
+++ b/pkg/server/handler.go
@@ -8,6 +8,7 @@ import (
"git.giftfish.de/ston1th/docstore/pkg/log"
"git.giftfish.de/ston1th/docstore/pkg/otp"
"git.giftfish.de/ston1th/jwt/v3"
+ "html/template"
"net/http"
"path/filepath"
"strings"
@@ -178,10 +179,13 @@ func indexHandler(ctx *Context) {
path := strings.TrimPrefix(ctx.Path(), core.IndexPrefix)
switch ctx.Method() {
case "GET":
+ earlyErr := make(chan error)
go func() {
- file, txt, err := ctx.Srv.Tesseract.Scan(path)
+ file, txt, err := ctx.Srv.Scanner.Scan(path)
if err != nil {
- log.Printf("scan: %s: %s", path, err)
+ select {
+ case earlyErr <- err:
+ }
return
}
id, err := ctx.Srv.DB.Index.Add(txt, "pdf")
@@ -195,6 +199,19 @@ func indexHandler(ctx *Context) {
}
}()
p, _ := filepath.Split(path)
+ p = filepath.Dir(p)
+ select {
+ case err := <-earlyErr:
+ ctx.Template("iseHandler")
+ ctx.Data = webData{
+ Data: template.HTML(strings.ReplaceAll(err.Error(), "\n", "
")),
+ Path: p,
+ }
+ ctx.Status(http.StatusInternalServerError)
+ ctx.Exec()
+ return
+ case <-time.After(time.Second):
+ }
ctx.Redirect(p, http.StatusFound)
}
}
@@ -231,35 +248,35 @@ func moveHandler(ctx *Context) {
func deleteHandler(ctx *Context) {
path := strings.TrimPrefix(ctx.Path(), core.DeletePrefix)
+ mode, err := ctx.Srv.FS.Mode(path)
+ if err != nil {
+ ctx.Error(err)
+ return
+ }
+ switch mode {
+ case fs.IsFile:
+ ctx.Data = webData{
+ Title: "Delete File",
+ BodyTitle: "Delete File",
+ }
+ case fs.IsDir:
+ ctx.Data = webData{
+ Title: "Delete Directory",
+ BodyTitle: "Delete Directory",
+ }
+ default:
+ ctx.Data = webData{
+ Title: "Delete",
+ BodyTitle: "Delete",
+ }
+ ctx.Error(noSuchFile)
+ return
+ }
+ ctx.Data.Path = path
+ ctx.Data.Data = mode
ctx.Template("deleteHandler")
switch ctx.Method() {
case "GET":
- mode, err := ctx.Srv.FS.Mode(path)
- if err != nil {
- ctx.Error(err)
- return
- }
- switch mode {
- case fs.IsFile:
- ctx.Data = webData{
- Title: "Delete File",
- BodyTitle: "Delete File",
- }
- case fs.IsDir:
- ctx.Data = webData{
- Title: "Delete Directory",
- BodyTitle: "Delete Directory",
- }
- default:
- ctx.Data = webData{
- Title: "Delete",
- BodyTitle: "Delete",
- }
- ctx.Error(noSuchFile)
- return
- }
- ctx.Data.Path = path
- ctx.Data.Data = mode
ctx.Exec()
case "POST":
files, err := ctx.Srv.FS.Delete(path)
@@ -272,21 +289,22 @@ func deleteHandler(ctx *Context) {
if err != nil {
log.Printf("db: delete: %s %s", f, err)
}
+ if id == "" {
+ continue
+ }
err = ctx.Srv.DB.Index.Delete(id)
if err != nil {
log.Printf("index: delete: %s %s", f, err)
}
}
- ctx.Redirect(core.IndexURI, http.StatusFound)
+ p, _ := filepath.Split(path)
+ p = filepath.Dir(p)
+ ctx.Redirect(p, http.StatusFound)
}
}
func dirHandler(ctx *Context) {
ctx.Template("dirHandler")
- ctx.Data = webData{
- Title: "Directory Viewer",
- BodyTitle: "Directory Viewer",
- }
path := ctx.Path()
switch ctx.Method() {
case "GET":
@@ -297,12 +315,18 @@ func dirHandler(ctx *Context) {
}
switch mode {
case fs.IsDir:
+ ctx.Data = webData{
+ Title: "Directory Viewer",
+ }
dir := ctx.Srv.FS.ReadDir(path)
ctx.Srv.DB.GetIndexed(dir.Files)
ctx.Data.Data = dir
ctx.Data.Paths = fs.Paths(path)
ctx.Exec()
case fs.IsFile:
+ ctx.Data = webData{
+ Title: "Document Viewer",
+ }
ctx.Template("fileHandler")
ctx.Data.Data = core.RawPrefix + path
ctx.Data.Paths = fs.Paths(path)
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 4a67705..7da6c44 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -10,7 +10,7 @@ import (
"git.giftfish.de/ston1th/docstore/pkg/db"
"git.giftfish.de/ston1th/docstore/pkg/fs"
"git.giftfish.de/ston1th/docstore/pkg/log"
- "git.giftfish.de/ston1th/docstore/pkg/tesseract"
+ "git.giftfish.de/ston1th/docstore/pkg/scan"
"git.giftfish.de/ston1th/godrop/v2"
"git.giftfish.de/ston1th/jwt/v3"
"github.com/gorilla/mux"
@@ -31,10 +31,10 @@ type HTTPServer struct {
srv *http.Server
handler http.Handler
- DB *db.DB
- JWT *jwt.JWT
- Tesseract *tesseract.Tesseract
- FS *fs.Filesystem
+ DB *db.DB
+ JWT *jwt.JWT
+ Scanner *scan.Scanner
+ FS *fs.Filesystem
templ map[string]*template.Template
res map[string][]byte
@@ -56,7 +56,7 @@ func (s *HTTPServer) Start() (err error) {
if err != nil {
return errors.New("fs: " + err.Error())
}
- s.Tesseract, err = tesseract.NewTesseract(s.Config.DataDir, nil, time.Second*30)
+ s.Scanner, err = scan.New(s.Config.DataDir, nil, time.Second*30)
if err != nil {
return errors.New("tesseract: " + err.Error())
}
diff --git a/pkg/server/templates.go b/pkg/server/templates.go
index bf64c2f..15118c2 100644
--- a/pkg/server/templates.go
+++ b/pkg/server/templates.go
@@ -28,7 +28,8 @@ const (
{{if eq .Data 1}}
This will also remove all files and directories below.
| Path | +Type | Options | ||
|---|---|---|---|---|
| {{$item.Name}}/ | -Move | +Directory | +||
| {{$item.Name}} | +File |