Skip to content
Snippets Groups Projects
Commit f13c7df5 authored by ale's avatar ale
Browse files

Use subcommands for cli tool

parent 825467fc
No related branches found
No related tags found
No related merge requests found
package main
import (
"context"
"flag"
"log"
"os"
"github.com/google/subcommands"
)
// Dump the database.
type dumpCommand struct{}
func (c *dumpCommand) Name() string { return "dump-db" }
func (c *dumpCommand) Synopsis() string { return "Dump the database" }
func (c *dumpCommand) Usage() string {
return `dump-db
Dump the database.
Requires exclusive access to the db, so the HTTP server must not be
running at the same time.
`
}
func (c *dumpCommand) SetFlags(f *flag.FlagSet) {}
func (c *dumpCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
db := openDB()
defer db.Close()
if err := db.Dump(os.Stdout); err != nil {
log.Printf("error: %v", err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}
// Restore the database.
type restoreCommand struct{}
func (c *restoreCommand) Name() string { return "dump-db" }
func (c *restoreCommand) Synopsis() string { return "Dump the database" }
func (c *restoreCommand) Usage() string {
return `dump-db
Dump the database.
Requires exclusive access to the db, so the HTTP server must not be
running at the same time.
`
}
func (c *restoreCommand) SetFlags(f *flag.FlagSet) {}
func (c *restoreCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
db := openDB()
defer db.Close()
if err := db.Restore(os.Stdin); err != nil {
log.Printf("error: %v", err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}
// Reindex the database.
type reindexCommand struct{}
func (c *reindexCommand) Name() string { return "reindex" }
func (c *reindexCommand) Synopsis() string { return "Regenerate the full-text search index" }
func (c *reindexCommand) Usage() string {
return `reindex
Regenerate the full-text search index.
`
}
func (c *reindexCommand) SetFlags(f *flag.FlagSet) {}
func (c *reindexCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
db := openDB()
defer db.Close()
if err := db.Reindex(); err != nil {
log.Printf("error: %v", err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}
package main
import (
"bufio"
"bytes"
"errors"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"git.autistici.org/ale/liber"
)
// Various ways to ask a user to choose something.
// Only make one user prompt at a time.
var promptMutex sync.Mutex
// Prompt user using stdin. Kind of annoying because it interferes
// with logging on stderr. It is used as a fallback.
func promptUserStdin(path string, choices []*liber.Metadata) *liber.Metadata {
promptMutex.Lock()
defer promptMutex.Unlock()
fmt.Printf("\n[*] Possible matches for %s:\n\n", path)
for idx, md := range choices {
fmt.Printf(" %d) %s\n", idx+1, md.String())
}
prompt := "Pick one, or Enter to skip: "
rdr := bufio.NewReader(os.Stdin)
for {
fmt.Printf(prompt)
os.Stdout.Sync()
result, err := rdr.ReadString('\n')
if err != nil || result == "\n" {
break
}
idx, err := strconv.Atoi(strings.TrimSpace(result))
if err != nil {
fmt.Printf("%v\n", err)
continue
}
if idx < 1 || idx > len(choices) {
fmt.Printf("Insert a number between 1 and %d.\n", len(choices))
continue
}
return choices[idx-1]
}
return nil
}
func findProgram(progs []string) (string, error) {
for _, p := range progs {
if path, err := exec.LookPath(p); err == nil {
return path, nil
}
}
return "", errors.New("not found")
}
var dialogProg string
func findDialogProg() bool {
dialogProgs := []string{"whiptail", "dialog"}
if os.Getenv("DISPLAY") != "" {
dialogProgs = append([]string{"gdialog", "xdialog"}, dialogProgs...)
}
if p, err := findProgram(dialogProgs); err == nil {
dialogProg = p
}
if dialogProg == "" {
return false
}
return true
}
// Prompt user using 'dialog', or a graphical variant if X11 is detected.
func promptUserDialog(path string, choices []*liber.Metadata) *liber.Metadata {
promptMutex.Lock()
defer promptMutex.Unlock()
args := []string{
"--title", "Metadata Chooser",
"--menu", fmt.Sprintf("Possible matches for %s:", path),
"0", "0", "0",
}
for idx, md := range choices {
args = append(args, strconv.Itoa(idx+1))
args = append(args, md.String())
}
var output bytes.Buffer
cmd := exec.Command(dialogProg, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = &output
if err := cmd.Run(); err != nil {
// If the user selects 'Cancel', dialog will exit with
// status 1.
log.Printf("dialog failed: %v", err)
return nil
}
result, err := strconv.Atoi(strings.TrimSpace(output.String()))
if err != nil {
return nil
}
return choices[result-1]
}
func promptUser(noninteractive bool) func(string, []*liber.Metadata) *liber.Metadata {
if noninteractive {
return func(path string, choices []*liber.Metadata) *liber.Metadata {
return nil
}
}
if findDialogProg() {
return promptUserDialog
}
return promptUserStdin
}
package main
import (
"bufio"
"bytes"
"errors"
"context"
"flag"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/google/subcommands"
"git.autistici.org/ale/liber"
"git.autistici.org/ale/liber/util"
)
var (
databaseDir = flag.String("db-dir", "~/.liber", "database directory")
bookDir = flag.String("book-dir", "", "books directory")
update = flag.Bool("update", false, "update the db")
search = flag.Bool("search", false, "search something")
remotesync = flag.String("sync", "", "push data to remote server")
reindex = flag.Bool("reindex", false, "re-create the search index")
httpserver = flag.String("http-server", "", "start the HTTP server on the specified address")
noninteractive = flag.Bool("noninteractive", false, "disable interactive metadata prompts on update")
)
// Various ways to ask a user to choose something.
// Prompt user using stdin. Kind of annoying because it interferes
// with logging on stderr. It is used as a fallback.
func promptUserStdin(path string, choices []*liber.Metadata) (*liber.Metadata, error) {
fmt.Printf("\n[*] Possible matches for %s:\n\n", path)
for idx, md := range choices {
fmt.Printf(" %d) %s\n", idx+1, md.String())
}
prompt := "Pick one, or Enter to skip: "
rdr := bufio.NewReader(os.Stdin)
for {
fmt.Printf(prompt)
os.Stdout.Sync()
result, err := rdr.ReadString('\n')
if err != nil || result == "\n" {
break
}
idx, err := strconv.Atoi(strings.TrimSpace(result))
if err != nil {
fmt.Printf("%v\n", err)
continue
}
if idx < 1 || idx > len(choices) {
fmt.Printf("Insert a number between 1 and %d.\n", len(choices))
continue
}
return choices[idx-1], nil
}
return nil, nil
}
func findProgram(progs []string) (string, error) {
for _, p := range progs {
if path, err := exec.LookPath(p); err == nil {
return path, nil
}
}
return "", errors.New("not found")
}
var (
dialogProg string
dialogProgInit bool
databaseDir = flag.String("db-dir", "~/.liber", "database directory")
bookDir = flag.String("book-dir", "", "books directory")
)
func getDialogProg() (string, error) {
if !dialogProgInit {
dialogProgInit = true
dialogProgs := []string{"whiptail", "dialog"}
if os.Getenv("DISPLAY") != "" {
dialogProgs = append([]string{"gdialog", "xdialog"}, dialogProgs...)
}
if p, err := findProgram(dialogProgs); err == nil {
dialogProg = p
}
}
if dialogProg == "" {
return "", errors.New("not found")
}
return dialogProg, nil
}
// Prompt user using 'dialog', or a graphical variant if X11 is detected.
func promptUserDialog(path string, choices []*liber.Metadata) (*liber.Metadata, error) {
dialog, err := getDialogProg()
if err != nil {
return nil, err
}
args := []string{
"--title", "Metadata Chooser",
"--menu", fmt.Sprintf("Possible matches for %s:", path),
"0", "0", "0",
}
for idx, md := range choices {
args = append(args, strconv.Itoa(idx+1))
args = append(args, md.String())
}
var output bytes.Buffer
cmd := exec.Command(dialog, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = &output
if err := cmd.Run(); err != nil {
// If the user selects 'Cancel', dialog will exit with
// status 1.
log.Printf("dialog failed: %v", err)
return nil, nil
}
result, err := strconv.Atoi(strings.TrimSpace(output.String()))
if err != nil {
return nil, nil
}
return choices[result-1], nil
}
var promptMutex sync.Mutex
func promptUser(path string, choices []*liber.Metadata) *liber.Metadata {
if *noninteractive {
return nil
}
promptMutex.Lock()
defer promptMutex.Unlock()
result, err := promptUserDialog(path, choices)
if err != nil {
result, err = promptUserStdin(path, choices)
if err != nil {
return nil
}
}
return result
}
func doUpdate(db *liber.Database, dir string) {
db.Update(dir, promptUser)
}
func doSync(db *liber.Database, remoteAddr string) {
storage := liber.NewFileStorage(util.ExpandTilde(*bookDir))
sc := liber.NewRemoteServer(remoteAddr)
if err := db.Sync(storage, sc); err != nil {
log.Fatal(err)
}
}
func doSearch(db *liber.Database, query string) {
results, err := db.Search(query, 0, 100)
func openDB() *liber.Database {
dbdir := util.ExpandTilde(*databaseDir)
db, err := liber.NewDb(dbdir)
if err != nil {
log.Fatal(err)
}
if results.NumResults == 0 {
fmt.Printf("No results.\n")
} else {
fmt.Printf("%d results found:\n\n", results.NumResults)
for i, book := range results.Results {
fmt.Printf("%d) %s\n", i+1, book.Metadata.String())
if files, err := db.GetBookFiles(book.Id); err == nil {
for _, f := range files {
fmt.Printf(" %s: %s\n", strings.TrimPrefix(f.FileType, "."), f.Path)
}
}
fmt.Printf("\n")
}
}
}
func doHttpServer(db *liber.Database, addr string) {
storage := liber.NewRWFileStorage(util.ExpandTilde(*bookDir), 2)
cache := liber.NewRWFileStorage(filepath.Join(util.ExpandTilde(*databaseDir), "cache"), 2)
server := liber.NewHttpServer(db, storage, cache, addr)
log.Fatal(server.ListenAndServe())
}
func doReindex(db *liber.Database) {
log.Println("starting database indexing")
if err := db.Reindex(); err != nil {
log.Fatal(err)
}
log.Println("database indexing complete")
}
func b2i(b bool) int {
if b {
return 1
}
return 0
return db
}
func main() {
subcommands.Register(subcommands.HelpCommand(), "")
subcommands.Register(subcommands.FlagsCommand(), "")
subcommands.Register(subcommands.CommandsCommand(), "")
subcommands.Register(&updateCommand{}, "")
subcommands.Register(&searchCommand{}, "")
subcommands.Register(&syncCommand{}, "")
subcommands.Register(&serverCommand{}, "")
subcommands.Register(&dumpCommand{}, "Maintenance")
subcommands.Register(&restoreCommand{}, "Maintenance")
subcommands.Register(&reindexCommand{}, "Maintenance")
log.SetFlags(0)
flag.Parse()
nset := b2i(*update) + b2i(*search) + b2i(*httpserver != "") + b2i(*remotesync != "") + b2i(*reindex)
if nset != 1 {
log.Fatal("Must specify one of --update, --sync, --search, --reindex or --http-server")
}
if *bookDir == "" {
log.Fatal("Must specify --book-dir")
}
dbdir := util.ExpandTilde(*databaseDir)
db, err := liber.NewDb(dbdir)
if err != nil {
log.Fatal(err)
}
defer db.Close()
if *update {
// Redirect logging to dbdir/update.log.
logf, err := os.OpenFile(filepath.Join(dbdir, "update.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
if err == nil {
defer logf.Close()
log.SetOutput(logf)
log.SetFlags(log.Ldate | log.Ltime)
}
doUpdate(db, util.ExpandTilde(*bookDir))
} else if *remotesync != "" {
doSync(db, *remotesync)
} else if *search {
query := strings.Join(flag.Args(), " ")
if query == "" {
log.Fatal("No query specified")
}
doSearch(db, query)
} else if *reindex {
doReindex(db)
} else if *httpserver != "" {
doHttpServer(db, *httpserver)
}
os.Exit(int(subcommands.Execute(context.Background())))
}
package main
import (
"context"
"flag"
"fmt"
"log"
"strings"
"github.com/google/subcommands"
)
type searchCommand struct{}
func (c *searchCommand) SetFlags(f *flag.FlagSet) {}
func (c *searchCommand) Name() string { return "search" }
func (c *searchCommand) Synopsis() string { return "Search the database" }
func (c *searchCommand) Usage() string {
return `search <QUERY>
Search the local database.
`
}
func (c *searchCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
query := strings.Join(f.Args(), " ")
if query == "" {
log.Printf("Must specify a query")
return subcommands.ExitUsageError
}
db := openDB()
defer db.Close()
results, err := db.Search(query, 0, 100)
if err != nil {
log.Printf("error: %v", err)
return subcommands.ExitFailure
}
if results.NumResults == 0 {
fmt.Printf("No results.\n")
} else {
fmt.Printf("%d results found:\n\n", results.NumResults)
for i, book := range results.Results {
fmt.Printf("%d) %s\n", i+1, book.Metadata.String())
if files, err := db.GetBookFiles(book.Id); err == nil {
for _, f := range files {
fmt.Printf(" %s: %s\n", strings.TrimPrefix(f.FileType, "."), f.Path)
}
}
fmt.Printf("\n")
}
}
return subcommands.ExitSuccess
}
package main
import (
"context"
"flag"
"log"
"path/filepath"
"github.com/google/subcommands"
"git.autistici.org/ale/liber"
"git.autistici.org/ale/liber/util"
)
type serverCommand struct {
addr string
}
func (c *serverCommand) Name() string { return "server" }
func (c *serverCommand) Synopsis() string { return "Run the HTTP server" }
func (c *serverCommand) Usage() string {
return `server [<OPTIONS>]
Run the HTTP server.
`
}
func (c *serverCommand) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.addr, "addr", ":3001", "address to listen on")
}
func (c *serverCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
db := openDB()
defer db.Close()
storage := liber.NewRWFileStorage(util.ExpandTilde(*bookDir), 2)
cache := liber.NewRWFileStorage(filepath.Join(util.ExpandTilde(*databaseDir), "cache"), 2)
server := liber.NewHttpServer(db, storage, cache, c.addr)
if err := server.ListenAndServe(); err != nil {
log.Printf("error: %v", err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}
package main
import (
"context"
"flag"
"log"
"github.com/google/subcommands"
"git.autistici.org/ale/liber"
"git.autistici.org/ale/liber/util"
)
type syncCommand struct{}
func (c *syncCommand) SetFlags(f *flag.FlagSet) {}
func (c *syncCommand) Name() string { return "sync" }
func (c *syncCommand) Synopsis() string { return "Synchronize with remote database" }
func (c *syncCommand) Usage() string {
return `sync <URL>
Push local content to a remote database.
`
}
func (c *syncCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if f.NArg() == 0 {
log.Printf("Must specify the remote URL")
return subcommands.ExitUsageError
} else if f.NArg() > 1 {
log.Printf("Too many arguments")
return subcommands.ExitUsageError
}
db := openDB()
defer db.Close()
storage := liber.NewFileStorage(util.ExpandTilde(*bookDir))
sc := liber.NewRemoteServer(f.Arg(0))
if err := db.Sync(storage, sc); err != nil {
log.Printf("sync failed: %v", err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}
package main
import (
"context"
"flag"
"log"
"os"
"path/filepath"
"github.com/google/subcommands"
"git.autistici.org/ale/liber/util"
)
type updateCommand struct {
noninteractive bool
}
func (c *updateCommand) SetFlags(f *flag.FlagSet) {
f.BoolVar(&c.noninteractive, "noninteractive", false, "disable user prompts")
}
func (c *updateCommand) Name() string { return "update" }
func (c *updateCommand) Synopsis() string { return "Add books to the local db" }
func (c *updateCommand) Usage() string {
return `update [<OPTIONS>]
Add books to the local database.
`
}
func (c *updateCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if f.NArg() > 0 {
log.Printf("Too many arguments")
return subcommands.ExitUsageError
}
db := openDB()
defer db.Close()
// Redirect logging to dbdir/update.log.
logf, err := os.OpenFile(filepath.Join(*databaseDir, "update.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
if err == nil {
defer logf.Close()
log.SetOutput(logf)
log.SetFlags(log.Ldate | log.Ltime)
}
db.Update(util.ExpandTilde(*bookDir), promptUser(c.noninteractive))
return subcommands.ExitSuccess
}
// Raw backup/restore tool for the 'liber' database. Mostly useful
// when upgrading the backend to an on-disk incompatible version.
//
package main
import (
"encoding/binary"
"flag"
"io"
"log"
"os"
"git.autistici.org/ale/liber"
"git.autistici.org/ale/liber/util"
)
var (
databaseDir = flag.String("db-dir", "~/.liber", "database directory")
doDump = flag.Bool("dump", false, "dump the database")
doRestore = flag.Bool("restore", false, "restore the database")
)
var buckets = [][]byte{
liber.BookBucket,
liber.FileBucket,
liber.BookFileBucket,
}
func writeBytes(w io.Writer, b []byte) error {
sz := uint64(len(b))
if err := binary.Write(w, binary.BigEndian, sz); err != nil {
return err
}
_, err := w.Write(b)
return err
}
func readBytes(r io.Reader) ([]byte, error) {
var sz uint64
if err := binary.Read(r, binary.BigEndian, &sz); err != nil {
return nil, err
}
b := make([]byte, sz)
_, err := r.Read(b)
return b, err
}
func writeRecord(w io.Writer, key, value []byte) error {
if err := writeBytes(w, key); err != nil {
return err
}
return writeBytes(w, value)
}
func readRecord(r io.Reader) ([]byte, []byte, error) {
key, err := readBytes(r)
if err != nil {
return nil, nil, err
}
value, err := readBytes(r)
if err != nil {
return nil, nil, err
}
return key, value, nil
}
func backup(db *liber.Database, w io.Writer) error {
for _, bkt := range buckets {
i := db.Scan(bkt)
for i.Next() {
if err := writeRecord(w, i.RawKey(), i.RawValue()); err != nil {
return err
}
}
if err := i.Close(); err != nil {
return err
}
}
return nil
}
func restore(db *liber.Database, r io.Reader) error {
nrec := 0
for {
key, value, err := readRecord(r)
if err == io.EOF {
break
} else if err != nil {
return err
}
if err := db.RawPut(key, value); err != nil {
return err
}
nrec++
}
log.Printf("restored %d records, reindexing...", nrec)
if err := db.Reindex(); err != nil {
return err
}
log.Printf("done")
return nil
}
func main() {
log.SetFlags(0)
flag.Parse()
dbdir := util.ExpandTilde(*databaseDir)
db, err := liber.NewDb(dbdir)
if err != nil {
log.Fatal(err)
}
defer db.Close()
switch {
case *doDump:
err = backup(db, os.Stdout)
case *doRestore:
err = restore(db, os.Stdin)
}
if err != nil {
log.Fatal(err)
}
}
......@@ -20,7 +20,7 @@ override_dh_install:
install -m 755 -o root -g root -d $(PKGDIR)/usr/share/liber
-mkdir build
(export GOPATH=$(CURDIR)/build ; mkdir -p build/src/$(shell dirname $(DH_GOPKG)) ; ln -s $(CURDIR) build/src/$(DH_GOPKG) ; cd build/src/$(DH_GOPKG) && go install -v ./...)
(for f in liber liberdbtool ; do \
(for f in liber ; do \
install -m 755 -o root -g root build/bin/$$f $(PKGDIR)/usr/bin/$$f ; done)
(umask 022; cp -R --preserve=timestamps htdocs $(PKGDIR)/usr/share/liber/htdocs)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment