diff --git a/cmd/liber/liber.go b/cmd/liber/liber.go index 59819b7092afbd4e87d7559679b8f46710f3ae14..10c55409b680a5b29d691312a9e2e302230f4c7d 100644 --- a/cmd/liber/liber.go +++ b/cmd/liber/liber.go @@ -13,13 +13,13 @@ import ( ) var ( - databaseDir = flag.String("db-dir", "~/.liber", "database directory") - bookDir = flag.String("book-dir", "", "books directory") + databaseDir = flag.String("db", "~/.liber", "database directory") + bookDir = flag.String("books", "", "books directory") ) func openDB() *liber.Database { dbdir := util.ExpandTilde(*databaseDir) - db, err := liber.NewDb(dbdir) + db, err := liber.NewDb("leveldb", dbdir) if err != nil { log.Fatal("error opening database: ", err) } diff --git a/database.go b/database.go index b5fc26570a6c6a41ae82fd0942e3d202dc35cd64..508cce4c9758670012b2b70c696b4775e92ff2d2 100644 --- a/database.go +++ b/database.go @@ -29,31 +29,38 @@ import ( blevequery "github.com/blevesearch/bleve/search/query" ) +// BookId is a unique identifier assigned to each book. type BookId uint64 func (id BookId) String() string { return strconv.FormatUint(uint64(id), 10) } +// Key returns a binary-encoded version of the identifier. func (id BookId) Key() []byte { var b [8]byte binary.LittleEndian.PutUint64(b[:], uint64(id)) return b[:] } +// NewID creates a new random unique book identifier. func NewID() BookId { return BookId(rand.Int63()) } +// ParseID parses a string with a hex-encoded representation of a +// book identifier. func ParseID(s string) BookId { id, _ := strconv.ParseUint(s, 10, 64) return BookId(id) } +// ParseBinaryID parses a binary-encoded book identifier. func ParseBinaryID(b []byte) BookId { return BookId(binary.LittleEndian.Uint64(b)) } +// Book holds information about a book. type Book struct { Id BookId CoverPath string @@ -64,6 +71,7 @@ func (b *Book) String() string { return fmt.Sprintf("%s (%s)", b.Metadata.String(), b.Id.String()) } +// File holds information about a file that is archived in our storage. type File struct { Path string FileType string @@ -73,6 +81,8 @@ type File struct { Id BookId } +// HasChanged returns true if the file metadata has changed compared +// to what is passed as argument. func (f *File) HasChanged(info os.FileInfo) bool { return !info.ModTime().Equal(f.Mtime) || info.Size() != f.Size } @@ -197,6 +207,8 @@ type backend interface { ScanFiles() <-chan *File } +// Database binds together a database of books/files with a searchable +// full-text index, making sure that updates are synchronized. type Database struct { backend @@ -204,6 +216,8 @@ type Database struct { index bleve.Index } +// NewDb creates a new Database at the specified location. The only +// supported backendType at the moment is 'leveldb'. func NewDb(backendType, path string) (*Database, error) { // Make sure that path exists. if _, err := os.Stat(path); err != nil { @@ -227,6 +241,7 @@ func NewDb(backendType, path string) (*Database, error) { d := &Database{backend: b, path: path} // Initialize the index. if err := d.setupIndex(filepath.Join(path, "index")); err != nil { + b.Close() return nil, err } return d, nil @@ -251,11 +266,14 @@ func (db *Database) setupIndex(path string) error { return nil } +// Close the database, the index, and all associated resources. func (db *Database) Close() { db.index.Close() db.backend.Close() } +// PutBook stores a new book in the database (or updates an existing +// one). func (db *Database) PutBook(b *Book) error { if err := db.backend.PutBook(b); err != nil { return err @@ -263,6 +281,7 @@ func (db *Database) PutBook(b *Book) error { return db.index.Index(b.Id.String(), flatten(b)) } +// DeleteBook removes a book from the database. func (db *Database) DeleteBook(bookid BookId) error { db.backend.DeleteBook(bookid) return db.index.Delete(bookid.String()) @@ -438,6 +457,8 @@ func (db *Database) onAllBooks(f func(*Book) error) error { return nil } +// SearchResult holds the result of a search query. NumResults refers +// to the total number of results, regardless of pagination. type SearchResult struct { Results []*Book NumResults int @@ -464,7 +485,7 @@ func (db *Database) Search(queryStr string, offset, limit int) (*SearchResult, e return db.doSearch(bleve.NewQueryStringQuery(queryStr), offset, limit) } -// Autocomplete runs a fuzzy search for a term. +// Suggest runs a fuzzy search for a term, for autocompletion. func (db *Database) Suggest(term string) (*SearchResult, error) { query := bleve.NewTermQuery(term) query.SetField("_suggest") diff --git a/files.go b/files.go index 6cd3f537563bb66f0ea5ab73b4cacbb07042a444..dbe60475314d3488091a68ca7d408f9c6e0387c2 100644 --- a/files.go +++ b/files.go @@ -17,13 +17,14 @@ type FileStorage struct { Root string } +// NewFileStorage creates a new FileStorage with the specified root path. func NewFileStorage(root string) *FileStorage { return &FileStorage{ Root: root, } } -// Return the absolute path of a file, given its relative path. +// Abs returns the absolute path of a file, given its relative path. func (s *FileStorage) Abs(path string) string { if strings.HasPrefix(path, "/") { return path @@ -31,7 +32,7 @@ func (s *FileStorage) Abs(path string) string { return filepath.Join(s.Root, path) } -// Return the relative path of a file with respect to the storage +// Rel returns the relative path of a file with respect to the storage // root. func (s *FileStorage) Rel(abspath string) (string, error) { return filepath.Rel(s.Root, abspath) @@ -63,8 +64,9 @@ func (s *FileStorage) Rename(oldpath, newpath string) error { return os.Rename(s.Abs(oldpath), s.Abs(newpath)) } -func (s *FileStorage) Walk(w *util.Walker, fn filepath.WalkFunc) { - w.Walk(s.Root, func(path string, info os.FileInfo, ferr error) error { +// Walk the file hierarchy. +func (s *FileStorage) Walk(w *util.Walker, fn filepath.WalkFunc) error { + return w.Walk(s.Root, func(path string, info os.FileInfo, ferr error) error { if ferr != nil { return nil } @@ -83,6 +85,8 @@ type RWFileStorage struct { Nesting int } +// NewRWFileStorage creates a new RWFileStorage with the given +// directory nesting factor. func NewRWFileStorage(root string, nesting int) *RWFileStorage { return &RWFileStorage{ FileStorage: NewFileStorage(root), diff --git a/isbn_detect.go b/isbn_detect.go index 57017092f3e0e8b680d2265629b12c728ba2faa8..ef2adf59b4135ff770c85bb3aef72f8b208e73b3 100644 --- a/isbn_detect.go +++ b/isbn_detect.go @@ -53,7 +53,7 @@ func findISBNInPage(r io.Reader) []string { func validateIsbn10(isbn string) bool { var sum int - var multiply int = 10 + multiply := 10 for i, v := range isbn { if v == '-' { continue diff --git a/metadata.go b/metadata.go index c54d4ef2e9b25afc99af22d17076f1d8cd0f41b7..c51da867d8a1e0e7225f38fd9585107a593149d1 100644 --- a/metadata.go +++ b/metadata.go @@ -8,18 +8,18 @@ import ( "regexp" "strings" - "github.com/meskio/epubgo" "git.autistici.org/ale/liber/third_party/gobipocket" + "github.com/meskio/epubgo" ) -// A metadata provider generates metadata from the local filesystem. +// A MetadataProvider generates metadata from the local filesystem. type MetadataProvider interface { Name() string Lookup(*FileStorage, string, string) (*Metadata, error) GetBookCover(*FileStorage, string) (string, error) } -// A metadata refiner improves on existing metadata and may provide +// A MetadataRefiner improves on existing metadata and may provide // more than one result to choose from. It usually involves talking to // a remote service. type MetadataRefiner interface { @@ -28,11 +28,13 @@ type MetadataRefiner interface { GetBookCover(*Metadata) ([]byte, error) } +// MetadataSource identifies a document within a specific source. type MetadataSource struct { Name string ID string } +// Metadata for a book. type Metadata struct { Title string Date string @@ -290,6 +292,8 @@ func (p *fileProvider) Name() string { return "file" } +// GetFileType returns a file's type (i.e. the extension), if it is +// one of the supported ones. func GetFileType(path string) (string, error) { filetype := strings.ToLower(filepath.Ext(path)) if filetype != ".epub" && filetype != ".mobi" && filetype != ".pdf" { diff --git a/sync.go b/sync.go index b16ced567eec76fbcea559b993923f052b084901..315a3eebb814fc83a4a3df9e4f1b0e3c6f45097e 100644 --- a/sync.go +++ b/sync.go @@ -120,27 +120,27 @@ func (r *remoteServer) SendBook(book *Book, storage *FileStorage, files []*File) if err != nil { return err } - if err := json.NewEncoder(part).Encode(book.Metadata); err != nil { + if err = json.NewEncoder(part).Encode(book.Metadata); err != nil { return err } for i, f := range files { varname := fmt.Sprintf("book%d", i) filename := fmt.Sprintf("%d%s", book.Id, f.FileType) - if err := addFilePart(w, varname, storage, f.Path, filename); err != nil { + if err = addFilePart(w, varname, storage, f.Path, filename); err != nil { w.Close() return err } } if book.CoverPath != "" { - if err := addFilePart(w, "cover", storage, book.CoverPath, "cover.jpg"); err != nil { + if err = addFilePart(w, "cover", storage, book.CoverPath, "cover.jpg"); err != nil { w.Close() return err } } - if err := w.Close(); err != nil { + if err = w.Close(); err != nil { return err } diff --git a/update.go b/update.go index fc28c2f3c1be1dbe6324a1c22b0906de5f8e9a4c..6c0bbd6694b051efca489ccff2a4d18a2090465d 100644 --- a/update.go +++ b/update.go @@ -10,10 +10,13 @@ import ( ) const ( - SourceDB = 1 << iota - SourceFS + sourceDB = 1 << iota + sourceFS ) +// MetadataChooserFunc is a function that should allow the user to +// select one of multiple candidates for metadata resolution. The +// function can be interactive. type MetadataChooserFunc func(string, []*Metadata) *Metadata type fileData struct { @@ -51,7 +54,7 @@ type updateContext struct { func (uc *updateContext) dbFileScanner(fileCh chan fileData) { for f := range uc.db.ScanFiles() { fileCh <- fileData{ - source: SourceDB, + source: sourceDB, path: f.Path, id: f.Id, } @@ -59,14 +62,20 @@ func (uc *updateContext) dbFileScanner(fileCh chan fileData) { } func (uc *updateContext) localFileScanner(basedir string, fileCh chan fileData) { - uc.storage.Walk(util.NewDefaultWalker(), func(path string, info os.FileInfo, err error) error { + err := uc.storage.Walk(util.NewDefaultWalker(), func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } fileCh <- fileData{ - source: SourceFS, + source: sourceFS, path: path, info: info, } return nil }) + if err != nil { + log.Printf("walk error: %v", err) + } } func (uc *updateContext) differ(basedir string) chan fileData { @@ -93,9 +102,9 @@ func (uc *updateContext) differ(basedir string) chan fileData { // Merge the two sources and keep track of files that // only appear in the database but not on the // filesystem, so we can remove them at the end. - // All entries with source == SourceFS will be sent to + // All entries with source == sourceFS will be sent to // the output channel in any case. - allSources := SourceDB | SourceFS + allSources := sourceDB | sourceFS tmp := make(map[string]int) for f := range fileCh { tmp[f.path] |= f.source @@ -104,12 +113,12 @@ func (uc *updateContext) differ(basedir string) chan fileData { if tmp[f.path] == allSources { delete(tmp, f.path) } - if f.source == SourceFS { + if f.source == sourceFS { outCh <- f } } for path, value := range tmp { - if value == SourceDB { + if value == sourceDB { log.Printf("file %s has been removed", path) uc.db.DeleteFile(path) } @@ -283,6 +292,12 @@ var ( const numUpdateMetadataWorkers = 10 +// Update the database with the contents of the storage at dir. If +// there are multiple options for metadata resolution, invoke the +// provided MetadataChooserFunc. +// +// This will walk the source filesystem, detect changed files, and +// trigger the metadata resolution process. func (db *Database) Update(dir string, chooser MetadataChooserFunc) { // Parallelize metadata extraction, serialize database updates // (so that index-based de-duplication works).