From e410e47ae44a232e3010c1f2b595941d3d7e2102 Mon Sep 17 00:00:00 2001 From: Radhi Fadlillah Date: Sat, 3 Feb 2018 15:02:28 +0700 Subject: [PATCH] Add command for updating data --- cmd/open.go | 4 +- cmd/print.go | 2 +- cmd/update.go | 176 +++++++++++++++++++++++++++++++++++++++++++ database/database.go | 4 +- database/sqlite.go | 145 ++++++++++++++++++++++------------- model/model.go | 5 +- 6 files changed, 276 insertions(+), 60 deletions(-) create mode 100644 cmd/update.go diff --git a/cmd/open.go b/cmd/open.go index 6b3e73e..e94368d 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -49,7 +49,7 @@ func init() { func openBookmarks(args ...string) { // Read bookmarks from database - bookmarks, err := DB.GetBookmarks(args...) + bookmarks, err := DB.GetBookmarks(false, args...) if err != nil { cError.Println(err) return @@ -75,7 +75,7 @@ func openBookmarks(args ...string) { func openBookmarksCache(trimSpace bool, args ...string) { // Read bookmark content from database - bookmarks, err := DB.GetBookmarksContent(args...) + bookmarks, err := DB.GetBookmarks(true, args...) if err != nil { cError.Println(err) return diff --git a/cmd/print.go b/cmd/print.go index 14dfdcc..24a3d27 100644 --- a/cmd/print.go +++ b/cmd/print.go @@ -22,7 +22,7 @@ var ( indexOnly, _ := cmd.Flags().GetBool("index-only") // Read bookmarks from database - bookmarks, err := DB.GetBookmarks(args...) + bookmarks, err := DB.GetBookmarks(false, args...) if err != nil { cError.Println(err) os.Exit(1) diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 0000000..f1a1675 --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,176 @@ +package cmd + +import ( + "fmt" + "github.com/RadhiFadlillah/go-readability" + "github.com/RadhiFadlillah/shiori/model" + "github.com/spf13/cobra" + "strconv" + "strings" + "sync" + "time" +) + +var ( + updateCmd = &cobra.Command{ + Use: "update [indices]", + Short: "Update the saved bookmarks.", + Long: "Update fields of an existing bookmark. " + + "Accepts space-separated list of indices (e.g. 5 6 23 4 110 45), hyphenated range (e.g. 100-200) or both (e.g. 1-3 7 9). " + + "If no arguments, ALL bookmarks will be updated. Update works differently depending on the flags:\n" + + "- If --title, --tag or --comment is passed without any value, clear the corresponding field from DB.\n" + + "- If indices are passed without any flags (--url, --title, --tag and --excerpt), read the URLs from DB and update titles from web.\n" + + "- If --url is passed (and --title is omitted), update the title from web using the URL. While using this flag, update only accept EXACTLY one index.\n" + + "While updating bookmark's tags, you can use - to remove tag (e.g. -nature to remove nature tag from this bookmark).", + Run: func(cmd *cobra.Command, args []string) { + // Read flags + url, _ := cmd.Flags().GetString("url") + title, _ := cmd.Flags().GetString("title") + excerpt, _ := cmd.Flags().GetString("excerpt") + tags, _ := cmd.Flags().GetStringSlice("tags") + offline, _ := cmd.Flags().GetBool("offline") + skipConfirmation, _ := cmd.Flags().GetBool("yes") + + // Check if --url flag is used + if url != "" { + if len(args) != 1 { + cError.Println("Update only accepts one index while using --url flag") + return + } + + idx, err := strconv.Atoi(args[0]) + if err != nil || idx < -1 { + cError.Println("Index is not valid") + return + } + } + + // If no arguments, confirm to user + if len(args) == 0 && !skipConfirmation { + confirmUpdate := "" + fmt.Print("Update ALL bookmarks? (y/n): ") + fmt.Scanln(&confirmUpdate) + + if confirmUpdate != "y" { + fmt.Println("No bookmarks updated") + return + } + } + + // Read bookmarks from database + bookmarks, err := DB.GetBookmarks(true, args...) + if err != nil { + cError.Println(err) + return + } + + if len(bookmarks) == 0 { + cError.Println("No matching index found") + return + } + + if url != "" && len(bookmarks) == 1 { + bookmarks[0].URL = url + } + + // If not offline, fetch articles from internet + if !offline { + mutex := sync.Mutex{} + waitGroup := sync.WaitGroup{} + + for i, book := range bookmarks { + go func(pos int, book model.Bookmark) { + waitGroup.Add(1) + defer waitGroup.Done() + + article, err := readability.Parse(book.URL, 10*time.Second) + if err == nil { + book.Title = article.Meta.Title + book.ImageURL = article.Meta.Image + book.Excerpt = article.Meta.Excerpt + book.Author = article.Meta.Author + book.MinReadTime = article.Meta.MinReadTime + book.MaxReadTime = article.Meta.MaxReadTime + book.Content = article.Content + book.HTML = article.RawContent + + mutex.Lock() + bookmarks[pos] = book + mutex.Unlock() + } + }(i, book) + } + + waitGroup.Wait() + } + + // Map the tags to be deleted + addedTags := make(map[string]struct{}) + deletedTags := make(map[string]struct{}) + for _, tag := range tags { + tag = strings.ToLower(tag) + tag = strings.TrimSpace(tag) + + if strings.HasPrefix(tag, "-") { + tag = strings.TrimPrefix(tag, "-") + deletedTags[tag] = struct{}{} + } else { + addedTags[tag] = struct{}{} + } + } + + // Set default title, excerpt and tags + for i := range bookmarks { + if title != "" { + bookmarks[i].Title = title + } + + if excerpt != "" { + bookmarks[i].Excerpt = excerpt + } + + tempAddedTags := make(map[string]struct{}) + for key, value := range addedTags { + tempAddedTags[key] = value + } + + newTags := []model.Tag{} + for _, tag := range bookmarks[i].Tags { + if _, isDeleted := deletedTags[tag.Name]; isDeleted { + tag.Deleted = true + } + + if _, alreadyExist := addedTags[tag.Name]; alreadyExist { + delete(tempAddedTags, tag.Name) + } + + newTags = append(newTags, tag) + } + + for tag := range tempAddedTags { + newTags = append(newTags, model.Tag{Name: tag}) + } + + bookmarks[i].Tags = newTags + } + + err = DB.UpdateBookmarks(bookmarks) + if err != nil { + cError.Println("Failed to update bookmarks:", err) + return + } + + printBookmark(bookmarks...) + }, + } +) + +func init() { + updateCmd.Flags().StringP("url", "u", "", "New URL for this bookmark.") + updateCmd.Flags().StringP("title", "i", "", "New title for this bookmark.") + updateCmd.Flags().StringP("excerpt", "e", "", "New excerpt for this bookmark.") + updateCmd.Flags().StringSliceP("tags", "t", []string{}, "Comma-separated tags for this bookmark.") + updateCmd.Flags().BoolP("offline", "o", false, "Update bookmark without fetching data from internet.") + updateCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt and update ALL bookmarks") + rootCmd.AddCommand(updateCmd) +} diff --git a/database/database.go b/database/database.go index b0e46ea..0c3edf6 100644 --- a/database/database.go +++ b/database/database.go @@ -7,10 +7,10 @@ import ( type Database interface { SaveBookmark(bookmark model.Bookmark) (int64, error) - GetBookmarks(indices ...string) ([]model.Bookmark, error) + GetBookmarks(withContent bool, indices ...string) ([]model.Bookmark, error) DeleteBookmarks(indices ...string) ([]int, []int, error) SearchBookmarks(keyword string, tags ...string) ([]model.Bookmark, error) - GetBookmarksContent(indices ...string) ([]model.Bookmark, error) + UpdateBookmarks(bookmarks []model.Bookmark) error } func checkError(err error) { diff --git a/database/sqlite.go b/database/sqlite.go index 0722c3c..bb6a0db 100644 --- a/database/sqlite.go +++ b/database/sqlite.go @@ -156,7 +156,7 @@ func (db *SQLiteDatabase) SaveBookmark(bookmark model.Bookmark) (bookmarkID int6 return bookmarkID, err } -func (db *SQLiteDatabase) GetBookmarks(indices ...string) ([]model.Bookmark, error) { +func (db *SQLiteDatabase) GetBookmarks(withContent bool, indices ...string) ([]model.Bookmark, error) { // Convert list of index to int listIndex := []int{} errInvalidIndex := fmt.Errorf("Index is not valid") @@ -214,23 +214,35 @@ func (db *SQLiteDatabase) GetBookmarks(indices ...string) ([]model.Bookmark, err return nil, err } - // Fetch tags for each bookmarks + // Fetch tags and contents for each bookmarks stmtGetTags, err := db.Preparex(`SELECT t.id, t.name FROM bookmark_tag bt LEFT JOIN tag t ON bt.tag_id = t.id WHERE bt.bookmark_id = ? ORDER BY t.name`) if err != nil { return nil, err } - defer stmtGetTags.Close() - for i := range bookmarks { - tags := []model.Tag{} - err = stmtGetTags.Select(&tags, bookmarks[i].ID) + stmtGetContent, err := db.Preparex(`SELECT title, content, html FROM bookmark_content WHERE docid = ?`) + if err != nil { + return nil, err + } + + defer stmtGetTags.Close() + defer stmtGetContent.Close() + + for i, book := range bookmarks { + book.Tags = []model.Tag{} + err = stmtGetTags.Select(&book.Tags, book.ID) if err != nil && err != sql.ErrNoRows { return nil, err } - bookmarks[i].Tags = tags + err = stmtGetContent.Get(&book, book.ID) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + + bookmarks[i] = book } return bookmarks, nil @@ -428,59 +440,86 @@ func (db *SQLiteDatabase) SearchBookmarks(keyword string, tags ...string) ([]mod return bookmarks, nil } -func (db *SQLiteDatabase) GetBookmarksContent(indices ...string) ([]model.Bookmark, error) { - // Convert list of index to int - listIndex := []int{} - errInvalidIndex := fmt.Errorf("Index is not valid") +func (db *SQLiteDatabase) UpdateBookmarks(bookmarks []model.Bookmark) (err error) { + // Prepare transaction + tx, err := db.Beginx() + if err != nil { + return err + } - for _, strIndex := range indices { - if strings.Contains(strIndex, "-") { - parts := strings.Split(strIndex, "-") - if len(parts) != 2 { - return nil, errInvalidIndex + // Make sure to rollback if panic ever happened + defer func() { + if r := recover(); r != nil { + panicErr, _ := r.(error) + tx.Rollback() + err = panicErr + } + }() + + // Prepare statement + stmtUpdateBookmark, err := db.Preparex(`UPDATE bookmark SET + url = ?, title = ?, image_url = ?, excerpt = ?, author = ?, + min_read_time = ?, max_read_time = ? WHERE id = ?`) + checkError(err) + + stmtUpdateBookmarkContent, err := db.Preparex(`UPDATE bookmark_content SET + title = ?, content = ?, html = ? WHERE docid = ?`) + checkError(err) + + stmtGetTag, err := tx.Preparex(`SELECT id FROM tag WHERE name = ?`) + checkError(err) + + stmtInsertTag, err := tx.Preparex(`INSERT INTO tag (name) VALUES (?)`) + checkError(err) + + stmtInsertBookmarkTag, err := tx.Preparex(`INSERT OR IGNORE INTO bookmark_tag (tag_id, bookmark_id) VALUES (?, ?)`) + checkError(err) + + stmtDeleteBookmarkTag, err := tx.Preparex(`DELETE FROM bookmark_tag WHERE bookmark_id = ? AND tag_id = ?`) + checkError(err) + + for _, book := range bookmarks { + stmtUpdateBookmark.MustExec( + book.URL, + book.Title, + book.ImageURL, + book.Excerpt, + book.Author, + book.MinReadTime, + book.MaxReadTime, + book.ID) + + stmtUpdateBookmarkContent.MustExec( + book.Title, + book.Content, + book.HTML, + book.ID) + + for _, tag := range book.Tags { + if tag.Deleted { + stmtDeleteBookmarkTag.MustExec(book.ID, tag.ID) + continue } - minIndex, errMin := strconv.Atoi(parts[0]) - maxIndex, errMax := strconv.Atoi(parts[1]) - if errMin != nil || errMax != nil || minIndex < 1 || minIndex > maxIndex { - return nil, errInvalidIndex - } + if tag.ID == 0 { + tagID := int64(-1) + err = stmtGetTag.Get(&tagID, tag.Name) + checkError(err) - for i := minIndex; i <= maxIndex; i++ { - listIndex = append(listIndex, i) - } - } else { - index, err := strconv.Atoi(strIndex) - if err != nil || index < 1 { - return nil, errInvalidIndex - } + if tagID == -1 { + res := stmtInsertTag.MustExec(tag.Name) + tagID, err = res.LastInsertId() + checkError(err) + } - listIndex = append(listIndex, index) + stmtInsertBookmarkTag.Exec(tagID, book.ID) + } } } - // Prepare where clause - args := []interface{}{} - whereClause := " WHERE 1" + // Commit transaction + err = tx.Commit() + checkError(err) - if len(listIndex) > 0 { - whereClause = " WHERE docid IN (" - for _, idx := range listIndex { - args = append(args, idx) - whereClause += "?," - } - - whereClause = whereClause[:len(whereClause)-1] - whereClause += ")" - } - - bookmarks := []model.Bookmark{} - err := db.Select(&bookmarks, - `SELECT docid id, title, content, html - FROM bookmark_content`+whereClause, args...) - if err != nil && err != sql.ErrNoRows { - return nil, err - } - - return bookmarks, nil + return err } diff --git a/model/model.go b/model/model.go index 56c4671..d25951c 100644 --- a/model/model.go +++ b/model/model.go @@ -1,8 +1,9 @@ package model type Tag struct { - ID int64 `db:"id" json:"id"` - Name string `db:"name" json:"name"` + ID int64 `db:"id" json:"id"` + Name string `db:"name" json:"name"` + Deleted bool `json:"-"` } type Bookmark struct {