Commit 338c1055 authored by ale's avatar ale

Merge branch 'subcommands'

parents ea1a7128 f13c7df5
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
}
<