diff --git a/cmd/liber/db.go b/cmd/liber/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..5d301b9b6b204ddcadf482b649b6064534ad4c7b
--- /dev/null
+++ b/cmd/liber/db.go
@@ -0,0 +1,91 @@
+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
+}
diff --git a/cmd/liber/dialog.go b/cmd/liber/dialog.go
new file mode 100644
index 0000000000000000000000000000000000000000..d65af8329c75fadca645078cb5fa3a5beb57a641
--- /dev/null
+++ b/cmd/liber/dialog.go
@@ -0,0 +1,126 @@
+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
+}
diff --git a/cmd/liber/liber.go b/cmd/liber/liber.go
index 5e59652ae235760371c53f7dbcc18a9fb22b4d36..c5db157d740da7417587237e0e11ff03bb999b78 100644
--- a/cmd/liber/liber.go
+++ b/cmd/liber/liber.go
@@ -1,246 +1,45 @@
 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())))
 }
diff --git a/cmd/liber/search.go b/cmd/liber/search.go
new file mode 100644
index 0000000000000000000000000000000000000000..dd0610369c1f473142bccda19412d98198af2314
--- /dev/null
+++ b/cmd/liber/search.go
@@ -0,0 +1,56 @@
+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
+}
diff --git a/cmd/liber/server.go b/cmd/liber/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..751655e4cd958766bac3d14ff5272cfe87bea2bc
--- /dev/null
+++ b/cmd/liber/server.go
@@ -0,0 +1,45 @@
+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
+}
diff --git a/cmd/liber/sync.go b/cmd/liber/sync.go
new file mode 100644
index 0000000000000000000000000000000000000000..5b29209a1477cf4900e2b7c8a1bca09799ea4a14
--- /dev/null
+++ b/cmd/liber/sync.go
@@ -0,0 +1,45 @@
+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
+}
diff --git a/cmd/liber/update.go b/cmd/liber/update.go
new file mode 100644
index 0000000000000000000000000000000000000000..3125fa29d54740a8348fc6497ff8f2cd8403b58d
--- /dev/null
+++ b/cmd/liber/update.go
@@ -0,0 +1,52 @@
+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
+}
diff --git a/cmd/liberdbtool/liberdbtool.go b/cmd/liberdbtool/liberdbtool.go
deleted file mode 100644
index d3ce5431f905ca309a97016d80527d06db6aaef4..0000000000000000000000000000000000000000
--- a/cmd/liberdbtool/liberdbtool.go
+++ /dev/null
@@ -1,124 +0,0 @@
-// 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)
-	}
-}
diff --git a/debian/rules b/debian/rules
index d17d4bbfe89af097765707de3f59a32e3d788ac4..5b77ad03fafc8b70a7adae724a49bf6147bc2ccf 100755
--- a/debian/rules
+++ b/debian/rules
@@ -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)