433 lines
9.4 KiB
Go
433 lines
9.4 KiB
Go
// Copyright (C) 2021 Marius Schellenberger
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"syscall"
|
|
|
|
"git.giftfish.de/ston1th/docstore/pkg/core"
|
|
"git.giftfish.de/ston1th/docstore/pkg/db"
|
|
"git.giftfish.de/ston1th/docstore/pkg/scan"
|
|
"git.giftfish.de/ston1th/docstore/pkg/server"
|
|
"git.giftfish.de/ston1th/godrop/v2"
|
|
"github.com/go-logr/logr"
|
|
"github.com/urfave/cli"
|
|
"k8s.io/klog/v2"
|
|
"k8s.io/klog/v2/klogr"
|
|
//"github.com/go-logr/zerologr"
|
|
//"github.com/rs/zerolog"
|
|
)
|
|
|
|
const (
|
|
defRunDir = "/var/lib/docstore"
|
|
defListen = "127.0.0.1:8080"
|
|
defLogFile = "docstore.log"
|
|
|
|
defRunUser = "docstore"
|
|
defRunGroup = "docstore"
|
|
|
|
defTimeout = 600
|
|
|
|
tesseractThreads = 1
|
|
tesseractOEM = 1
|
|
)
|
|
|
|
var (
|
|
config core.Config
|
|
dumpFile string
|
|
configFile string
|
|
)
|
|
|
|
func initServer(cfg *core.Config) (err error) {
|
|
// remove initial pledge due to violation with go1.12
|
|
// sysctl kern.somaxconn
|
|
// if err = godrop.PledgePromises("stdio rpath wpath cpath inet fattr flock proc exec id unveil"); err != nil {
|
|
// return
|
|
// }
|
|
dropCfg := godrop.Config{
|
|
User: cfg.User,
|
|
Group: cfg.Group,
|
|
Foreground: cfg.Foreground,
|
|
}
|
|
err = godrop.Drop(dropCfg, func() (net.Listener, error) {
|
|
return net.Listen("tcp", cfg.ListenAddr)
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
if err = os.Chdir(cfg.RunDir); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = godrop.PledgePromises("stdio rpath wpath cpath inet fattr flock proc exec unveil"); err != nil {
|
|
return
|
|
}
|
|
|
|
l, err := godrop.GetListener()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
ll, err := logFileLogger(cfg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
log := ll.WithName("main")
|
|
log.Info("starting docstore", "version", cfg.Version)
|
|
|
|
cfg.DataDir = filepath.Join(cfg.RunDir, core.DataDir)
|
|
debugCfg := new(core.Config)
|
|
*debugCfg = *cfg
|
|
debugCfg.Secret = "[redacted]"
|
|
log.V(2).Info("docstore config", "cfg", debugCfg)
|
|
|
|
scanner, err := scan.New(ll.WithName("scanner"), cfg)
|
|
if err != nil {
|
|
return errors.New("scanner: " + err.Error())
|
|
}
|
|
err = godrop.Unveil(cfg.RunDir, "rwc")
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = godrop.UnveilBlock()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
srv := server.NewHTTPServer(ll, cfg, l, scanner)
|
|
err = srv.Start()
|
|
if err != nil {
|
|
return
|
|
}
|
|
sigs := make(chan os.Signal, 1)
|
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
|
<-sigs
|
|
log.Info("docstore shutdown initiated")
|
|
err = srv.Stop()
|
|
if err != nil {
|
|
log.Error(err, "shutdown")
|
|
}
|
|
log.Info("docstore shutdown completed")
|
|
return
|
|
}
|
|
|
|
func loadConfig() error {
|
|
if configFile == "" {
|
|
return nil
|
|
}
|
|
file, err := os.Open(configFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = json.NewDecoder(file).Decode(&config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if config.Debug {
|
|
new(klog.Level).Set("2")
|
|
//zerolog.SetGlobalLevel(zerolog.TraceLevel)
|
|
}
|
|
if config.RunDir == "" {
|
|
config.RunDir = defRunDir
|
|
}
|
|
if config.User == "" {
|
|
config.User = defRunUser
|
|
}
|
|
if config.Group == "" {
|
|
config.Group = defRunGroup
|
|
}
|
|
if config.ListenAddr == "" {
|
|
config.ListenAddr = defListen
|
|
}
|
|
if config.LogFile == "" {
|
|
config.LogFile = defLogFile
|
|
}
|
|
if config.CMDTimeout == 0 {
|
|
config.CMDTimeout = defTimeout
|
|
}
|
|
if config.TesseractThreads == 0 {
|
|
config.TesseractThreads = tesseractThreads
|
|
}
|
|
if config.TesseractOEM == 0 {
|
|
config.TesseractOEM = tesseractOEM
|
|
}
|
|
return file.Close()
|
|
}
|
|
|
|
func fatal(err error) {
|
|
log.Error(err, "init failed")
|
|
os.Exit(1)
|
|
}
|
|
|
|
var (
|
|
log logr.Logger
|
|
//zl zerolog.Logger
|
|
)
|
|
|
|
func logFileLogger(cfg *core.Config) (l logr.Logger, err error) {
|
|
f, err := os.OpenFile(filepath.Join(cfg.DataDir, cfg.LogFile), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o640)
|
|
if err != nil {
|
|
return
|
|
}
|
|
klog.LogToStderr(false)
|
|
klog.SetOutput(f)
|
|
l = klogr.New()
|
|
//zln := zl.Output(f)
|
|
//l = zerologr.New(&zln)
|
|
return
|
|
}
|
|
|
|
func init() {
|
|
//zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
|
|
// short := file
|
|
// for i := len(file) - 1; i > 0; i-- {
|
|
// if file[i] == '/' {
|
|
// short = file[i+1:]
|
|
// break
|
|
// }
|
|
// }
|
|
// file = short
|
|
// return file + ":" + strconv.Itoa(line)
|
|
//}
|
|
//zerologr.VerbosityFieldName = ""
|
|
//zl = zerolog.New(os.Stderr)
|
|
//zl = zl.With().Caller().Timestamp().Logger()
|
|
//log = zerologr.New(&zl)
|
|
log = klogr.New()
|
|
}
|
|
|
|
func Run(version string) {
|
|
app := cli.NewApp()
|
|
app.Name = "docstore"
|
|
app.Usage = "file server and indexing engine"
|
|
app.Version = version
|
|
app.Commands = []cli.Command{
|
|
{
|
|
Name: "server",
|
|
Usage: "start docstore server",
|
|
Flags: defaultFlags(serverFlags()),
|
|
Action: func(c *cli.Context) error {
|
|
config.Langs = c.StringSlice("langs")
|
|
if err := loadConfig(); err != nil {
|
|
fatal(err)
|
|
}
|
|
config.Version = app.Version
|
|
if err := initServer(&config); err != nil {
|
|
fatal(err)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "reset",
|
|
Usage: "reset the users password and TOTP token (docstore must be stopped)",
|
|
Flags: defaultFlags(nil),
|
|
Action: func(c *cli.Context) error {
|
|
if err := loadConfig(); err != nil {
|
|
fatal(err)
|
|
}
|
|
DB, err := db.NewPlain(log.WithName("db"), &config)
|
|
if err != nil {
|
|
fatal(err)
|
|
}
|
|
u, err := DB.GetUser()
|
|
if err != nil {
|
|
fatal(err)
|
|
}
|
|
fmt.Println("username:", u.Username)
|
|
pw, err := password()
|
|
if err != nil {
|
|
fatal(err)
|
|
}
|
|
if err = DB.UpdateUserPassword(pw); err != nil {
|
|
fatal(err)
|
|
}
|
|
fmt.Println("password:", pw)
|
|
if err = DB.UpdateUserSecret(""); err != nil {
|
|
fatal(err)
|
|
}
|
|
if err = DB.Close(); err != nil {
|
|
fatal(err)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "dump",
|
|
Usage: "create a json dump of all users (docstore must be stopped)",
|
|
Flags: defaultFlags(dumpRestoreFlags()),
|
|
Action: func(c *cli.Context) error {
|
|
var (
|
|
file = os.Stdout
|
|
err error
|
|
)
|
|
if err = loadConfig(); err != nil {
|
|
fatal(err)
|
|
}
|
|
if dumpFile != "-" {
|
|
file, err = os.OpenFile(dumpFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o640)
|
|
if err != nil {
|
|
fatal(err)
|
|
}
|
|
}
|
|
DB, err := db.NewPlain(log.WithName("db"), &config)
|
|
if err != nil {
|
|
fatal(err)
|
|
}
|
|
if err = DB.Dump(file); err != nil {
|
|
fatal(err)
|
|
}
|
|
file.Close()
|
|
err = DB.Close()
|
|
if err != nil {
|
|
fatal(err)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "restore",
|
|
Usage: "restore a json dump (docstore must be stopped)",
|
|
Flags: defaultFlags(dumpRestoreFlags()),
|
|
Action: func(c *cli.Context) error {
|
|
var (
|
|
file = os.Stdin
|
|
err error
|
|
)
|
|
if err = loadConfig(); err != nil {
|
|
fatal(err)
|
|
}
|
|
if dumpFile != "-" {
|
|
file, err = os.Open(dumpFile)
|
|
if err != nil {
|
|
fatal(err)
|
|
}
|
|
}
|
|
DB, err := db.NewPlain(log.WithName("db"), &config)
|
|
if err != nil {
|
|
fatal(err)
|
|
}
|
|
if err = DB.Restore(file); err != nil {
|
|
fatal(err)
|
|
}
|
|
file.Close()
|
|
err = DB.Close()
|
|
if err != nil {
|
|
fatal(err)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
app.Run(os.Args)
|
|
}
|
|
|
|
func serverFlags() []cli.Flag {
|
|
return []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "user, u",
|
|
Value: defRunUser,
|
|
Usage: "drop privileges to user",
|
|
Destination: &config.User,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "group, g",
|
|
Value: defRunGroup,
|
|
Usage: "drop privileges to group",
|
|
Destination: &config.Group,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "listen, l",
|
|
Value: defListen,
|
|
Usage: "listening <address>:<port>",
|
|
Destination: &config.ListenAddr,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "log",
|
|
Value: defLogFile,
|
|
Usage: "log file (use - for stdout, this only works in combination with -f)",
|
|
Destination: &config.LogFile,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "secret",
|
|
Usage: "static hmac secret (at least 64 chars, used for JWT signing)",
|
|
Destination: &config.Secret,
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "langs",
|
|
Usage: "languages for tesseract ocr (use --langs multiple times to add more languages)",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "timeout, t",
|
|
Value: defTimeout,
|
|
Usage: "timeout in seconds for executed teseract and pdftotext commands",
|
|
Destination: &config.CMDTimeout,
|
|
},
|
|
cli.IntFlag{
|
|
Name: "threads",
|
|
Value: tesseractThreads,
|
|
Usage: "tesseract thread limit (1-4)",
|
|
Destination: &config.TesseractThreads,
|
|
},
|
|
cli.IntFlag{
|
|
Name: "oem",
|
|
Value: tesseractOEM,
|
|
Usage: "tesseract oem value (1-4)",
|
|
Destination: &config.TesseractOEM,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "foreground, f",
|
|
Usage: "do not fork into the background",
|
|
Destination: &config.Foreground,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "secure, s",
|
|
Usage: "enable secure cookie flag",
|
|
Destination: &config.SecureCookie,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "debug",
|
|
Usage: "enable debugging",
|
|
Destination: &config.Debug,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "webdav",
|
|
Usage: "enable webdav file uploader",
|
|
Destination: &config.WebDav,
|
|
},
|
|
}
|
|
}
|
|
|
|
func dumpRestoreFlags() []cli.Flag {
|
|
return []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "file, f",
|
|
Value: "-",
|
|
Usage: "file to read/write dump (default stdin/stdout)",
|
|
Destination: &dumpFile,
|
|
},
|
|
}
|
|
}
|
|
|
|
func defaultFlags(f []cli.Flag) []cli.Flag {
|
|
return append([]cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "config, c",
|
|
Usage: "config file",
|
|
Destination: &configFile,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "run, r",
|
|
Value: defRunDir,
|
|
Usage: "run directory",
|
|
Destination: &config.RunDir,
|
|
},
|
|
}, f...)
|
|
}
|