resolving conflicts with master

This commit is contained in:
Simon Frostick 2018-03-08 13:58:14 +07:00
commit f00fb47845
16 changed files with 386 additions and 75 deletions

View file

@ -28,7 +28,7 @@ Shiori is a simple bookmarks manager written in Go language. Intended as a simpl
## Installation ## Installation
You can download the latest version of `shiori` from the release page, then put it in your `PATH`. If you want to build from source, make sure `go` is installed, then run : You can download the latest version of `shiori` from [the release page](https://github.com/RadhiFadlillah/shiori/releases/latest), then put it in your `PATH`. If you want to build from source, make sure `go` is installed, then run :
``` ```
go get github.com/RadhiFadlillah/shiori go get github.com/RadhiFadlillah/shiori

File diff suppressed because one or more lines are too long

View file

@ -2,6 +2,8 @@ package cmd
import ( import (
"fmt" "fmt"
"io"
"os"
"syscall" "syscall"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -45,7 +47,7 @@ var (
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
keyword, _ := cmd.Flags().GetString("search") keyword, _ := cmd.Flags().GetString("search")
err := printAccounts(keyword) err := printAccounts(keyword, os.Stdout)
if err != nil { if err != nil {
cError.Println(err) cError.Println(err)
return return
@ -103,7 +105,7 @@ func init() {
func addAccount(username, password string) error { func addAccount(username, password string) error {
if username == "" { if username == "" {
return fmt.Errorf("Username must not empty") return fmt.Errorf("Username must not be empty")
} }
if len(password) < 8 { if len(password) < 8 {
@ -118,15 +120,15 @@ func addAccount(username, password string) error {
return nil return nil
} }
func printAccounts(keyword string) error { func printAccounts(keyword string, wr io.Writer) error {
accounts, err := DB.GetAccounts(keyword, false) accounts, err := DB.GetAccounts(keyword, false)
if err != nil { if err != nil {
return err return err
} }
for _, account := range accounts { for _, account := range accounts {
cIndex.Print("- ") cIndex.Fprint(wr, "- ")
fmt.Println(account.Username) fmt.Fprintln(wr, account.Username)
} }
return nil return nil

50
cmd/account_test.go Normal file
View file

@ -0,0 +1,50 @@
package cmd
import (
"bytes"
"strings"
"testing"
)
func TestAddAccount(t *testing.T) {
tests := []struct {
username string
password string
want string
}{
{"", "", "Username must not be empty"},
{"abc", "abc", "Password must be at least"},
{"abc", "fooBar123", ""},
}
for _, tt := range tests {
err := addAccount(tt.username, tt.password)
if err != nil {
if tt.want == "" {
t.Errorf("got unexpected error: %v", err)
}
if !strings.Contains(err.Error(), tt.want) {
t.Errorf("expected error containing '%s', got error '%v'", err, tt.want)
}
continue
}
if tt.want != "" {
t.Errorf("expected error '%s', got no error", tt.want)
}
}
}
func TestPrintAccounts(t *testing.T) {
if err := addAccount("foo", "fooBar123"); err != nil {
t.Errorf("failed to add test account: %v", err)
return
}
b := bytes.NewBufferString("")
err := printAccounts("", b)
if err != nil {
t.Errorf("got unexpected error: %v", err)
}
got := b.String()
if !strings.Contains(got, "foo") {
t.Errorf("expected string containing 'foo', got '%s'", got)
}
}

View file

@ -1,7 +1,6 @@
package cmd package cmd
import ( import (
"html/template"
nurl "net/url" nurl "net/url"
"strings" "strings"
"time" "time"
@ -81,7 +80,7 @@ func addBookmark(base model.Bookmark, offline bool) (book model.Bookmark, err er
book.MinReadTime = article.Meta.MinReadTime book.MinReadTime = article.Meta.MinReadTime
book.MaxReadTime = article.Meta.MaxReadTime book.MaxReadTime = article.Meta.MaxReadTime
book.Content = article.Content book.Content = article.Content
book.HTML = template.HTML(article.RawContent) book.HTML = article.RawContent
if book.Title == "" { if book.Title == "" {
book.Title = article.Meta.Title book.Title = article.Meta.Title

73
cmd/add_test.go Normal file
View file

@ -0,0 +1,73 @@
package cmd
import (
"strings"
"testing"
"github.com/RadhiFadlillah/shiori/model"
)
func TestAddBookMark(t *testing.T) {
tests := []struct {
bookmark model.Bookmark
offline bool
want string
}{
{
model.Bookmark{},
true, "URL must not be empty",
},
{
model.Bookmark{
URL: "https://github.com/RadhiFadlillah/shiori",
},
true, "Title must not be empty",
},
{model.Bookmark{URL: "foo", Title: "Foo"}, true, ""},
{
model.Bookmark{
URL: "https://github.com/RadhiFadlillah/shiori",
Title: "Shiori",
},
true, "",
},
{
model.Bookmark{
URL: "https://github.com/RadhiFadlillah/shiori/issues",
},
false, "",
},
}
for _, tt := range tests {
bk, err := addBookmark(tt.bookmark, tt.offline)
if err != nil {
if tt.want == "" {
t.Errorf("got unexpected error: '%v'", err)
continue
}
if !strings.Contains(err.Error(), tt.want) {
t.Errorf("expected error '%s', got '%v'", tt.want, err)
}
continue
}
if tt.bookmark.URL == "" {
t.Errorf("expected error '%s', got '%v'", tt.want, err)
continue
}
if tt.offline && tt.bookmark.Title == "" {
t.Error("expected error 'Title must not be empty', got no error")
continue
}
if tt.want != "" {
t.Errorf("expected error '%s', got no error", tt.want)
continue
}
if tt.offline && bk.Title != tt.bookmark.Title {
t.Errorf("expected title '%s', got '%s'", tt.bookmark.Title, bk.Title)
}
if !tt.offline && bk.Title == "" {
t.Error("expected title, got empty string ''")
}
}
}

28
cmd/cmd_test.go Normal file
View file

@ -0,0 +1,28 @@
package cmd
import (
"fmt"
"os"
"testing"
db "github.com/RadhiFadlillah/shiori/database"
_ "github.com/mattn/go-sqlite3"
)
func TestMain(m *testing.M) {
testDBFile := "shiori_test.db"
sqliteDB, err := db.OpenSQLiteDatabase(testDBFile)
if err != nil {
fmt.Printf("failed to create tests DB: %v", err)
os.Exit(1)
}
DB = sqliteDB
code := m.Run()
if err := os.Remove(testDBFile); err != nil {
fmt.Printf("failed to delete tests DB: %v", err)
}
os.Exit(code)
}

View file

@ -41,8 +41,14 @@ var (
} }
// Prepare template // Prepare template
funcMap := template.FuncMap{
"html": func(s string) template.HTML {
return template.HTML(s)
},
}
tplFile, _ := assets.ReadFile("cache.html") tplFile, _ := assets.ReadFile("cache.html")
tplCache, err = template.New("cache.html").Parse(string(tplFile)) tplCache, err = template.New("cache.html").Funcs(funcMap).Parse(string(tplFile))
if err != nil { if err != nil {
cError.Println("Failed to generate HTML template") cError.Println("Failed to generate HTML template")
return return
@ -75,7 +81,13 @@ var (
port, _ := cmd.Flags().GetInt("port") port, _ := cmd.Flags().GetInt("port")
url := fmt.Sprintf(":%d", port) url := fmt.Sprintf(":%d", port)
logrus.Infoln("Serve shiori in", url) logrus.Infoln("Serve shiori in", url)
logrus.Fatalln(http.ListenAndServe(url, router)) svr := &http.Server{
Addr: url,
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 20 * time.Second,
}
logrus.Fatalln(svr.ListenAndServe())
}, },
} }
) )
@ -245,6 +257,10 @@ func apiInsertBookmarks(w http.ResponseWriter, r *http.Request, ps httprouter.Pa
} }
func apiUpdateBookmarks(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { func apiUpdateBookmarks(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get url parameter
_, dontOverwrite := r.URL.Query()["dont-overwrite"]
overwrite := !dontOverwrite
// Check token // Check token
err := checkAPIToken(r) err := checkAPIToken(r)
checkError(err) checkError(err)
@ -256,13 +272,9 @@ func apiUpdateBookmarks(w http.ResponseWriter, r *http.Request, ps httprouter.Pa
// Convert tags and ID // Convert tags and ID
id := []string{fmt.Sprintf("%d", request.ID)} id := []string{fmt.Sprintf("%d", request.ID)}
tags := make([]string, len(request.Tags))
for i, tag := range request.Tags {
tags[i] = tag.Name
}
// Update bookmark // Update bookmark
bookmarks, err := updateBookmarks(id, request.URL, request.Title, request.Excerpt, tags, false) bookmarks, err := updateBookmarks(id, request, false, overwrite)
checkError(err) checkError(err)
// Return new saved result // Return new saved result

View file

@ -2,7 +2,6 @@ package cmd
import ( import (
"fmt" "fmt"
"html/template"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -31,9 +30,10 @@ var (
tags, _ := cmd.Flags().GetStringSlice("tags") tags, _ := cmd.Flags().GetStringSlice("tags")
offline, _ := cmd.Flags().GetBool("offline") offline, _ := cmd.Flags().GetBool("offline")
skipConfirmation, _ := cmd.Flags().GetBool("yes") skipConfirmation, _ := cmd.Flags().GetBool("yes")
overwriteMetadata := !cmd.Flags().Changed("dont-overwrite")
// Check if --url flag is used // Check if --url flag is used
if url != "" { if cmd.Flags().Changed("url") {
if len(args) != 1 { if len(args) != 1 {
cError.Println("Update only accepts one index while using --url flag") cError.Println("Update only accepts one index while using --url flag")
return return
@ -46,6 +46,11 @@ var (
} }
} }
// Check if --excerpt flag is used
if !cmd.Flags().Changed("excerpt") {
excerpt = "empty"
}
// If no arguments, confirm to user // If no arguments, confirm to user
if len(args) == 0 && !skipConfirmation { if len(args) == 0 && !skipConfirmation {
confirmUpdate := "" confirmUpdate := ""
@ -59,7 +64,18 @@ var (
} }
// Update bookmarks // Update bookmarks
bookmarks, err := updateBookmarks(args, url, title, excerpt, tags, offline) base := model.Bookmark{
URL: url,
Title: title,
Excerpt: excerpt,
}
base.Tags = make([]model.Tag, len(tags))
for i, tag := range tags {
base.Tags[i] = model.Tag{Name: tag}
}
bookmarks, err := updateBookmarks(args, base, offline, overwriteMetadata)
if err != nil { if err != nil {
cError.Println(err) cError.Println(err)
return return
@ -77,14 +93,15 @@ func init() {
updateCmd.Flags().StringSliceP("tags", "t", []string{}, "Comma-separated tags 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("offline", "o", false, "Update bookmark without fetching data from internet.")
updateCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt and update ALL bookmarks") updateCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt and update ALL bookmarks")
updateCmd.Flags().Bool("dont-overwrite", false, "Don't overwrite existing metadata. Useful when only want to update bookmark's content.")
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
} }
func updateBookmarks(indices []string, url, title, excerpt string, tags []string, offline bool) ([]model.Bookmark, error) { func updateBookmarks(indices []string, base model.Bookmark, offline, overwrite bool) ([]model.Bookmark, error) {
mutex := sync.Mutex{} mutex := sync.Mutex{}
// Clear UTM parameters from URL // Clear UTM parameters from URL
url, err := clearUTMParams(url) url, err := clearUTMParams(base.URL)
if err != nil { if err != nil {
return []model.Bookmark{}, err return []model.Bookmark{}, err
} }
@ -114,14 +131,17 @@ func updateBookmarks(indices []string, url, title, excerpt string, tags []string
article, err := readability.Parse(book.URL, 10*time.Second) article, err := readability.Parse(book.URL, 10*time.Second)
if err == nil { if err == nil {
if overwrite {
book.Title = article.Meta.Title book.Title = article.Meta.Title
book.ImageURL = article.Meta.Image
book.Excerpt = article.Meta.Excerpt book.Excerpt = article.Meta.Excerpt
}
book.ImageURL = article.Meta.Image
book.Author = article.Meta.Author book.Author = article.Meta.Author
book.MinReadTime = article.Meta.MinReadTime book.MinReadTime = article.Meta.MinReadTime
book.MaxReadTime = article.Meta.MaxReadTime book.MaxReadTime = article.Meta.MaxReadTime
book.Content = article.Content book.Content = article.Content
book.HTML = template.HTML(article.RawContent) book.HTML = article.RawContent
mutex.Lock() mutex.Lock()
bookmarks[pos] = book bookmarks[pos] = book
@ -136,26 +156,26 @@ func updateBookmarks(indices []string, url, title, excerpt string, tags []string
// Map the tags to be deleted // Map the tags to be deleted
addedTags := make(map[string]struct{}) addedTags := make(map[string]struct{})
deletedTags := make(map[string]struct{}) deletedTags := make(map[string]struct{})
for _, tag := range tags { for _, tag := range base.Tags {
tag = strings.ToLower(tag) tagName := strings.ToLower(tag.Name)
tag = strings.TrimSpace(tag) tagName = strings.TrimSpace(tagName)
if strings.HasPrefix(tag, "-") { if strings.HasPrefix(tagName, "-") {
tag = strings.TrimPrefix(tag, "-") tagName = strings.TrimPrefix(tagName, "-")
deletedTags[tag] = struct{}{} deletedTags[tagName] = struct{}{}
} else { } else {
addedTags[tag] = struct{}{} addedTags[tagName] = struct{}{}
} }
} }
// Set default title, excerpt and tags // Set default title, excerpt and tags
for i := range bookmarks { for i := range bookmarks {
if title != "" { if base.Title != "" && overwrite {
bookmarks[i].Title = title bookmarks[i].Title = base.Title
} }
if excerpt != "" { if base.Excerpt != "empty" && overwrite {
bookmarks[i].Excerpt = excerpt bookmarks[i].Excerpt = base.Excerpt
} }
tempAddedTags := make(map[string]struct{}) tempAddedTags := make(map[string]struct{})

102
cmd/update_test.go Normal file
View file

@ -0,0 +1,102 @@
package cmd
import (
"fmt"
"strings"
"testing"
"github.com/RadhiFadlillah/shiori/model"
)
func TestUpdateBookMark(t *testing.T) {
testbks := []model.Bookmark{
{
URL: "https://github.com/RadhiFadlillah/shiori/releases",
Title: "Releases",
},
{
URL: "https://github.com/RadhiFadlillah/shiori/projects",
Title: "Projects",
},
}
for i, tb := range testbks {
bk, err := addBookmark(tb, true)
if err != nil {
t.Fatalf("failed to create testing bookmarks: %v", err)
}
testbks[i].ID = bk.ID
}
tests := []struct {
indices []string
url string
title string
excerpt string
tags []string
offline bool
want string
}{
{
indices: []string{"9000"},
want: "No matching index found",
},
{
indices: []string{"-1"},
want: "Index is not valid",
},
{
indices: []string{"3", "-1"},
want: "Index is not valid",
},
{
indices: []string{fmt.Sprintf("%d", testbks[0].ID)},
url: testbks[0].URL,
title: testbks[0].Title + " updated",
offline: true,
},
{
indices: []string{fmt.Sprintf("%d", testbks[0].ID)},
offline: false,
},
{
indices: []string{fmt.Sprintf("%d", testbks[1].ID)},
offline: true,
},
}
for _, tt := range tests {
base := model.Bookmark{
URL: tt.url,
Title: tt.title,
Excerpt: tt.excerpt,
}
base.Tags = make([]model.Tag, len(tt.tags))
for i, tag := range tt.tags {
base.Tags[i] = model.Tag{Name: tag}
}
bks, err := updateBookmarks(tt.indices, base, tt.offline, true)
if err != nil {
if tt.want == "" {
t.Errorf("got unexpected error: '%v'", err)
continue
}
if !strings.Contains(err.Error(), tt.want) {
t.Errorf("expected error '%s', got '%v'", tt.want, err)
}
continue
}
if tt.want != "" {
t.Errorf("expected error '%s', got no errors", tt.want)
continue
}
if len(bks) == 0 {
t.Error("expected at least 1 bookmark, got 0")
continue
}
bk := bks[0]
if tt.title == "" && bk.Title == tt.title {
t.Errorf("expected title as '%s', got '%s'", tt.title, bk.Title)
}
}
}

View file

@ -81,11 +81,11 @@ func OpenSQLiteDatabase(databasePath string) (*SQLiteDatabase, error) {
func (db *SQLiteDatabase) CreateBookmark(bookmark model.Bookmark) (bookmarkID int64, err error) { func (db *SQLiteDatabase) CreateBookmark(bookmark model.Bookmark) (bookmarkID int64, err error) {
// Check URL and title // Check URL and title
if bookmark.URL == "" { if bookmark.URL == "" {
return -1, fmt.Errorf("URL must not empty") return -1, fmt.Errorf("URL must not be empty")
} }
if bookmark.Title == "" { if bookmark.Title == "" {
return -1, fmt.Errorf("Title must not empty") return -1, fmt.Errorf("Title must not be empty")
} }
if bookmark.Modified == "" { if bookmark.Modified == "" {

View file

@ -1,7 +1,5 @@
package model package model
import "html/template"
// Tag is tag for the bookmark // Tag is tag for the bookmark
type Tag struct { type Tag struct {
ID int64 `db:"id" json:"id"` ID int64 `db:"id" json:"id"`
@ -22,7 +20,7 @@ type Bookmark struct {
MaxReadTime int `db:"max_read_time" json:"maxReadTime"` MaxReadTime int `db:"max_read_time" json:"maxReadTime"`
Modified string `db:"modified" json:"modified"` Modified string `db:"modified" json:"modified"`
Content string `db:"content" json:"-"` Content string `db:"content" json:"-"`
HTML template.HTML `db:"html" json:"-"` HTML string `db:"html" json:"-"`
Tags []Tag `json:"tags"` Tags []Tag `json:"tags"`
} }

View file

@ -34,7 +34,7 @@
{{end}} {{end}} {{end}} {{end}}
</div> </div>
<div id="content"> <div id="content">
{{.HTML}} {{html .HTML}}
</div> </div>
</div> </div>
<script> <script>

File diff suppressed because one or more lines are too long

View file

@ -96,7 +96,7 @@
</div> </div>
<template v-if="!displayTags"> <template v-if="!displayTags">
<div id="grid"> <div id="grid">
<div v-for="column in gridColumns" class="column"> <div v-for="column in gridColumns" class="column" :style="{maxWidth: columnWidth}">
<div v-for="item in column" class="bookmark" :class="{checked: isBookmarkChecked(item.index)}" :ref="'bookmark-'+item.index"> <div v-for="item in column" class="bookmark" :class="{checked: isBookmarkChecked(item.index)}" :ref="'bookmark-'+item.index">
<a class="checkbox" @click="toggleBookmarkCheck(item.index)"> <a class="checkbox" @click="toggleBookmarkCheck(item.index)">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
@ -404,19 +404,14 @@
}, },
updateBookmark: function (idx) { updateBookmark: function (idx) {
var bookmark = this.bookmarks[idx]; var bookmark = this.bookmarks[idx],
sendUpdateRequest = function (overwrite) {
var url = "/api/bookmarks";
if (!overwrite) url += "?dont-overwrite";
this.dialog.visible = true; instance.put(url, {
this.dialog.isError = false;
this.dialog.loading = false;
this.dialog.title = "Update Bookmark";
this.dialog.content = "Update data of <b>\"" + bookmark.title.trim() + "\"</b> ? This action is irreversible.";
this.dialog.mainChoice = "Yes";
this.dialog.secondChoice = "No";
this.dialog.mainAction = function () {
app.dialog.loading = true;
instance.put('/api/bookmarks', {
id: bookmark.id, id: bookmark.id,
excerpt: overwrite ? "empty" : bookmark.excerpt
}, { }, {
timeout: 15000, timeout: 15000,
}) })
@ -431,6 +426,28 @@
app.showDialogError("Error Updating Bookmark", errorMsg.trim()); app.showDialogError("Error Updating Bookmark", errorMsg.trim());
}); });
}; };
this.dialog.visible = true;
this.dialog.isError = false;
this.dialog.loading = false;
this.dialog.title = "Update Bookmark";
this.dialog.content = "Update data of <b>\"" + bookmark.title.trim() + "\"</b> ? This action is irreversible.";
this.dialog.mainChoice = "Yes";
this.dialog.secondChoice = "No";
this.dialog.mainAction = function () {
app.dialog.title = "Overwrite Metadata";
app.dialog.content = "Overwrite the existing bookmark's metadata ?";
app.dialog.mainChoice = "Yes";
app.dialog.secondChoice = "No";
app.dialog.mainAction = function () {
app.dialog.loading = true;
sendUpdateRequest(true);
};
app.dialog.secondAction = function () {
app.dialog.loading = true;
sendUpdateRequest(false);
};
};
this.dialog.secondAction = function () { this.dialog.secondAction = function () {
app.dialog.visible = false; app.dialog.visible = false;
app.$nextTick(function () { app.$nextTick(function () {
@ -585,6 +602,12 @@
return finalContent; return finalContent;
}, },
columnWidth: function () {
var nColumn = Math.round(this.windowWidth / 500),
percent = Math.round(100 / nColumn * 1000) / 1000;
return percent + "%";
}
}, },
watch: { watch: {
'inputBookmark.url': function (newURL) { 'inputBookmark.url': function (newURL) {

View file

@ -538,6 +538,8 @@
color: @fontColor; color: @fontColor;
font-size: 1.3em; font-size: 1.3em;
font-weight: 600; font-weight: 600;
text-overflow: ellipsis;
overflow: hidden;
} }
.bookmark-url { .bookmark-url {
.bookmark-time; .bookmark-time;
@ -586,6 +588,8 @@
.bookmark-excerpt { .bookmark-excerpt {
padding: 16px 16px 0; padding: 16px 16px 0;
color: @fontColor; color: @fontColor;
text-overflow: ellipsis;
overflow: hidden;
} }
.bookmark-tags { .bookmark-tags {
display: flex; display: flex;