From 1e099fbd27e8327d520e592a847edf799af007ae Mon Sep 17 00:00:00 2001 From: Radhi Fadlillah Date: Tue, 21 May 2019 10:31:40 +0700 Subject: [PATCH] Restructure project directories --- .gitignore | 5 ++++- go.mod | 2 ++ go.sum | 12 ++++++++++ internal/cmd/account-add.go | 13 +++++++++++ internal/cmd/account-delete.go | 17 +++++++++++++++ internal/cmd/account-print.go | 16 ++++++++++++++ internal/cmd/account.go | 20 +++++++++++++++++ internal/cmd/add.go | 20 +++++++++++++++++ internal/cmd/delete.go | 21 ++++++++++++++++++ internal/cmd/export.go | 15 +++++++++++++ internal/cmd/import.go | 15 +++++++++++++ internal/cmd/open.go | 22 +++++++++++++++++++ internal/cmd/pocket.go | 15 +++++++++++++ internal/cmd/print.go | 20 +++++++++++++++++ internal/cmd/root.go | 29 ++++++++++++++++++++++++ internal/cmd/search.go | 22 +++++++++++++++++++ internal/cmd/serve.go | 19 ++++++++++++++++ internal/cmd/update.go | 27 +++++++++++++++++++++++ internal/database/database.go | 5 +++++ internal/database/sqlite.go | 11 ++++++++++ internal/model/model.go | 40 ++++++++++++++++++++++++++++++++++ internal/webserver/server.go | 1 + main.go | 10 ++++++++- 23 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 internal/cmd/account-add.go create mode 100644 internal/cmd/account-delete.go create mode 100644 internal/cmd/account-print.go create mode 100644 internal/cmd/account.go create mode 100644 internal/cmd/add.go create mode 100644 internal/cmd/delete.go create mode 100644 internal/cmd/export.go create mode 100644 internal/cmd/import.go create mode 100644 internal/cmd/open.go create mode 100644 internal/cmd/pocket.go create mode 100644 internal/cmd/print.go create mode 100644 internal/cmd/root.go create mode 100644 internal/cmd/search.go create mode 100644 internal/cmd/serve.go create mode 100644 internal/cmd/update.go create mode 100644 internal/database/database.go create mode 100644 internal/database/sqlite.go create mode 100644 internal/model/model.go create mode 100644 internal/webserver/server.go diff --git a/.gitignore b/.gitignore index 4e68331..eef2841 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ # Exclude config file .vscode/ -*.toml \ No newline at end of file +*.toml + +# Exclude executable file +/shiori* \ No newline at end of file diff --git a/go.mod b/go.mod index 7b75501..1021a91 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/go-shiori/shiori go 1.12 require ( + github.com/jmoiron/sqlx v1.2.0 github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v0.0.4 + google.golang.org/appengine v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 99d7bf1..4107dab 100644 --- a/go.sum +++ b/go.sum @@ -7,12 +7,21 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -35,9 +44,12 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw= +google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/cmd/account-add.go b/internal/cmd/account-add.go new file mode 100644 index 0000000..ec32e50 --- /dev/null +++ b/internal/cmd/account-add.go @@ -0,0 +1,13 @@ +package cmd + +import "github.com/spf13/cobra" + +func accountAddCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "add username", + Short: "Create new account", + Args: cobra.ExactArgs(1), + } + + return cmd +} diff --git a/internal/cmd/account-delete.go b/internal/cmd/account-delete.go new file mode 100644 index 0000000..f035935 --- /dev/null +++ b/internal/cmd/account-delete.go @@ -0,0 +1,17 @@ +package cmd + +import "github.com/spf13/cobra" + +func accountDeleteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete [usernames]", + Short: "Delete the saved accounts", + Long: "Delete accounts. " + + "Accepts space-separated list of usernames. " + + "If no arguments, all records will be deleted.", + } + + cmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt and delete ALL accounts") + + return cmd +} diff --git a/internal/cmd/account-print.go b/internal/cmd/account-print.go new file mode 100644 index 0000000..9c00437 --- /dev/null +++ b/internal/cmd/account-print.go @@ -0,0 +1,16 @@ +package cmd + +import "github.com/spf13/cobra" + +func accountPrintCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "print", + Short: "Print the saved accounts", + Args: cobra.NoArgs, + Aliases: []string{"list", "ls"}, + } + + cmd.Flags().StringP("search", "s", "", "Search accounts by username") + + return cmd +} diff --git a/internal/cmd/account.go b/internal/cmd/account.go new file mode 100644 index 0000000..ef9e509 --- /dev/null +++ b/internal/cmd/account.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func accountCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "account", + Short: "Manage account for accessing web interface", + } + + cmd.AddCommand( + accountAddCmd(), + accountPrintCmd(), + accountDeleteCmd(), + ) + + return cmd +} diff --git a/internal/cmd/add.go b/internal/cmd/add.go new file mode 100644 index 0000000..d271616 --- /dev/null +++ b/internal/cmd/add.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func addCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "add url", + Short: "Bookmark the specified URL", + Args: cobra.ExactArgs(1), + } + + cmd.Flags().StringP("title", "i", "", "Custom title for this bookmark.") + cmd.Flags().StringP("excerpt", "e", "", "Custom excerpt for this bookmark.") + cmd.Flags().StringSliceP("tags", "t", []string{}, "Comma-separated tags for this bookmark.") + cmd.Flags().BoolP("offline", "o", false, "Save bookmark without fetching data from internet.") + + return cmd +} diff --git a/internal/cmd/delete.go b/internal/cmd/delete.go new file mode 100644 index 0000000..38acee1 --- /dev/null +++ b/internal/cmd/delete.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func deleteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete [indices]", + Short: "Delete the saved bookmarks", + Long: "Delete bookmarks. " + + "When a record is deleted, the last record is moved to the removed index. " + + "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 records will be deleted.", + Aliases: []string{"rm"}, + } + + cmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt and delete ALL bookmarks") + + return cmd +} diff --git a/internal/cmd/export.go b/internal/cmd/export.go new file mode 100644 index 0000000..c45244c --- /dev/null +++ b/internal/cmd/export.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func exportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "export target-file", + Short: "Export bookmarks into HTML file in Netscape Bookmark format", + Args: cobra.ExactArgs(1), + } + + return cmd +} diff --git a/internal/cmd/import.go b/internal/cmd/import.go new file mode 100644 index 0000000..8824a01 --- /dev/null +++ b/internal/cmd/import.go @@ -0,0 +1,15 @@ +package cmd + +import "github.com/spf13/cobra" + +func importCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "import source-file", + Short: "Import bookmarks from HTML file in Netscape Bookmark format", + Args: cobra.ExactArgs(1), + } + + cmd.Flags().BoolP("generate-tag", "t", false, "Auto generate tag from bookmark's category") + + return cmd +} diff --git a/internal/cmd/open.go b/internal/cmd/open.go new file mode 100644 index 0000000..4398117 --- /dev/null +++ b/internal/cmd/open.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func openCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "open [indices]", + Short: "Open the saved bookmarks", + Long: "Open bookmarks in browser. " + + "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 opened.", + } + + cmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt and open ALL bookmarks") + cmd.Flags().BoolP("cache", "c", false, "Open the bookmark's cache in text-only mode") + cmd.Flags().Bool("trim-space", false, "Trim all spaces and newlines from the bookmark's cache") + + return cmd +} diff --git a/internal/cmd/pocket.go b/internal/cmd/pocket.go new file mode 100644 index 0000000..de3539d --- /dev/null +++ b/internal/cmd/pocket.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func pocketCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "pocket source-file", + Short: "Import bookmarks from Pocket's exported HTML file", + Args: cobra.ExactArgs(1), + } + + return cmd +} diff --git a/internal/cmd/print.go b/internal/cmd/print.go new file mode 100644 index 0000000..1e1c36d --- /dev/null +++ b/internal/cmd/print.go @@ -0,0 +1,20 @@ +package cmd + +import "github.com/spf13/cobra" + +func printCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "print [indices]", + Short: "Print the saved bookmarks", + Long: "Show the saved bookmarks by its DB index. " + + "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 records with actual index from database are shown.", + Aliases: []string{"list", "ls"}, + } + + cmd.Flags().BoolP("json", "j", false, "Output data in JSON format") + cmd.Flags().BoolP("index-only", "i", false, "Only print the index of bookmarks") + + return cmd +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go new file mode 100644 index 0000000..14ba4ab --- /dev/null +++ b/internal/cmd/root.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// ShioriCmd returns the root command for shiori +func ShioriCmd() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "shiori", + Short: "Simple command-line bookmark manager built with Go", + } + + rootCmd.AddCommand( + accountCmd(), + addCmd(), + printCmd(), + searchCmd(), + updateCmd(), + deleteCmd(), + openCmd(), + importCmd(), + exportCmd(), + pocketCmd(), + serveCmd(), + ) + + return rootCmd +} diff --git a/internal/cmd/search.go b/internal/cmd/search.go new file mode 100644 index 0000000..44a33c3 --- /dev/null +++ b/internal/cmd/search.go @@ -0,0 +1,22 @@ +package cmd + +import "github.com/spf13/cobra" + +func searchCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "search keyword", + Short: "Search bookmarks by submitted keyword", + Long: "Search bookmarks by looking for matching keyword in bookmark's title and content. " + + "If no keyword submitted, print all saved bookmarks. " + + "Search results will be different depending on DBMS that used by shiori :\n" + + "- sqlite3, search works using fts4 method: https://www.sqlite.org/fts3.html.\n" + + "- mysql or mariadb, search works using natural language mode: https://dev.mysql.com/doc/refman/5.5/en/fulltext-natural-language.html.", + Args: cobra.MaximumNArgs(1), + } + + cmd.Flags().BoolP("json", "j", false, "Output data in JSON format") + cmd.Flags().BoolP("index-only", "i", false, "Only print the index of bookmarks") + cmd.Flags().StringSliceP("tags", "t", []string{}, "Search bookmarks with specified tag(s)") + + return cmd +} diff --git a/internal/cmd/serve.go b/internal/cmd/serve.go new file mode 100644 index 0000000..c1999b7 --- /dev/null +++ b/internal/cmd/serve.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func serveCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "serve", + Short: "Serve web interface for managing bookmarks", + Long: "Run a simple annd performant web server which " + + "serves the site for managing bookmarks. If --port " + + "flag is not used, it will use port 8080 by default.", + } + + cmd.Flags().IntP("port", "p", 8080, "Port that used by server") + + return cmd +} diff --git a/internal/cmd/update.go b/internal/cmd/update.go new file mode 100644 index 0000000..7ecedf6 --- /dev/null +++ b/internal/cmd/update.go @@ -0,0 +1,27 @@ +package cmd + +import "github.com/spf13/cobra" + +func updateCmd() *cobra.Command { + cmd := &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 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).", + } + + cmd.Flags().StringP("url", "u", "", "New URL for this bookmark.") + cmd.Flags().StringP("title", "i", "", "New title for this bookmark.") + cmd.Flags().StringP("excerpt", "e", "", "New excerpt for this bookmark.") + cmd.Flags().StringSliceP("tags", "t", []string{}, "Comma-separated tags for this bookmark.") + cmd.Flags().BoolP("offline", "o", false, "Update bookmark without fetching data from internet.") + cmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt and update ALL bookmarks") + cmd.Flags().Bool("dont-overwrite", false, "Don't overwrite existing metadata. Useful when only want to update bookmark's content.") + + return cmd +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..a7b306c --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,5 @@ +package database + +// DB is interface for accessing and manipulating data in database. +type DB interface { +} diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go new file mode 100644 index 0000000..4b9b2de --- /dev/null +++ b/internal/database/sqlite.go @@ -0,0 +1,11 @@ +package database + +import ( + "github.com/jmoiron/sqlx" +) + +// SQLiteDatabase is implementation of Database interface +// for connecting to SQLite3 database. +type SQLiteDatabase struct { + sqlx.DB +} diff --git a/internal/model/model.go b/internal/model/model.go new file mode 100644 index 0000000..1cab8fa --- /dev/null +++ b/internal/model/model.go @@ -0,0 +1,40 @@ +package model + +// Tag is the tag for a bookmark. +type Tag struct { + ID int `db:"id" json:"id"` + Name string `db:"name" json:"name"` + NBookmarks int `db:"n_bookmarks" json:"nBookmarks"` + Deleted bool `json:"-"` +} + +// Bookmark is the record for an URL. +type Bookmark struct { + ID int `db:"id" json:"id"` + URL string `db:"url" json:"url"` + Title string `db:"title" json:"title"` + ImageURL string `db:"image_url" json:"imageURL"` + Excerpt string `db:"excerpt" json:"excerpt"` + Author string `db:"author" json:"author"` + MinReadTime int `db:"min_read_time" json:"minReadTime"` + MaxReadTime int `db:"max_read_time" json:"maxReadTime"` + Modified string `db:"modified" json:"modified"` + Content string `db:"content" json:"-"` + HTML string `db:"html" json:"html,omitempty"` + HasContent bool `db:"has_content" json:"hasContent"` + Tags []Tag `json:"tags"` +} + +// Account is person that allowed to access web interface. +type Account struct { + ID int `db:"id" json:"id"` + Username string `db:"username" json:"username"` + Password string `db:"password" json:"password"` +} + +// LoginRequest is request from user to access web interface. +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` + Remember bool `json:"remember"` +} diff --git a/internal/webserver/server.go b/internal/webserver/server.go new file mode 100644 index 0000000..c5303c2 --- /dev/null +++ b/internal/webserver/server.go @@ -0,0 +1 @@ +package webserver diff --git a/main.go b/main.go index 4a9902a..d26bb9c 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,13 @@ package main +import ( + "github.com/go-shiori/shiori/internal/cmd" + "github.com/sirupsen/logrus" +) + func main() { - // + shioriCmd := cmd.ShioriCmd() + if err := shioriCmd.Execute(); err != nil { + logrus.Fatalln(err) + } }