diff --git a/cache/cache.go b/cache/cache.go index dd35d9a..340f8ca 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -2,6 +2,7 @@ package cache import ( "archive/zip" + "bufio" "fmt" "io" "io/ioutil" @@ -12,6 +13,7 @@ import ( "os/user" "path" "path/filepath" + "strconv" "strings" "time" ) @@ -19,6 +21,7 @@ import ( const ( indexJSON = "index.json" pagesDirectory = "pages" + historyPath = "/history" pageSuffix = ".md" zipPath = "/tldr.zip" ) @@ -32,6 +35,16 @@ type Repository struct { ttl time.Duration } +// HistoryRecord represent the search history of certain page +type HistoryRecord struct { + page string + count int +} + +func (h HistoryRecord) String() string { + return fmt.Sprintf("%s %d", h.page, h.count) +} + // NewRepository returns a new cache repository. The data is loaded from the // remote if missing or stale. func NewRepository(remote string, ttl time.Duration) (*Repository, error) { @@ -204,7 +217,21 @@ func (r *Repository) loadFromRemote() error { } func (r *Repository) makeCacheDir() error { - return os.MkdirAll(r.directory, 0755) + if err := os.MkdirAll(r.directory, 0755); err != nil { + return fmt.Errorf("ERROR: creating directory %s: %s", r.directory, err) + } + // touching the history file. + historyFile := path.Join(r.directory, historyPath) + return touchFile(historyFile) +} + +func touchFile(fileName string) error { + file, err := os.Create(fileName) + if err != nil { + return fmt.Errorf("ERROR: creating file %s: %s", fileName, err) + } + defer file.Close() + return nil } func (r *Repository) unzip() error { @@ -259,3 +286,87 @@ func (r Repository) isReachable() bool { _, err = net.DialTimeout("tcp", u.Hostname()+":"+u.Port(), timeout) return err == nil } + +func (r Repository) RecordHistory(page string) error { + records, err := r.LoadHistory() + if err != nil { + return fmt.Errorf("ERROR: loading history failed %s", err) + } + + newRecord := HistoryRecord{ + page: page, + count: 1, + } + + foundIdx := -1 + for idx, r := range *records { + if r.page == page { + newRecord.count = r.count + 1 + foundIdx = idx + break + } + } + + if foundIdx != -1 { //found in history, we want to put the last search at the end of the history. + newRecords := append((*records)[:foundIdx], (*records)[foundIdx+1:]...) + records = &newRecords + } + + newRecords := append(*records, newRecord) + return r.saveHistory(&newRecords) +} + +func (r Repository) saveHistory(history *[]HistoryRecord) error { + hisFile := path.Join(r.directory, historyPath) + inFile, err := os.Create(hisFile) + if err != nil { + return fmt.Errorf("ERROR: opening history file %s", hisFile) + } + defer inFile.Close() + + for _, his := range *history { + fmt.Fprintln(inFile, fmt.Sprintf("%s,%d", his.page, his.count)) + } + return nil +} + +func (r Repository) LoadHistory() (*[]HistoryRecord, error) { + // read the history file line by line, into a map. + history := path.Join(r.directory, historyPath) + //if it is not exist, touch it. + _, err := os.Stat(history) + + if os.IsNotExist(err) { + if err := touchFile(history); err != nil { + return nil, fmt.Errorf("ERROR: cannot create the history file %s, %s", history, err) + } + } + + inFile, err := os.Open(history) + if err != nil { + return nil, fmt.Errorf("ERROR: opening history file %s", history) + + } + + defer inFile.Close() + + scanner := bufio.NewScanner(inFile) + historyRecords := make([]HistoryRecord, 0, 10) + + for scanner.Scan() { + line := scanner.Text() + lineParts := strings.Split(line, ",") + count, err := strconv.Atoi(lineParts[1]) + + if err != nil { + return nil, err + } + + historyRecords = append(historyRecords, HistoryRecord{ + page: lineParts[0], + count: count, + }) + } + + return &historyRecords, nil +} diff --git a/cache/cache_test.go b/cache/cache_test.go index 58c78ae..a0e2f30 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -109,13 +109,13 @@ func TestMarkdown(t *testing.T) { _, err := r.Markdown("linux", "hostname") if err != nil { - t.Error("Exptected to successfully pull a page from the cache") + t.Error("Expected to successfully pull a page from the cache") } _, err = r.Markdown("linux", "hostnamee") if err == nil { - t.Error("Exptected to return an error for non existing pages") + t.Error("Expected to return an error for non existing pages") } } @@ -127,10 +127,70 @@ func TestPages(t *testing.T) { pages, err := r.Pages() if err != nil { - t.Error("Exptected to successfully retrieve all pages.") + t.Error("Expected to successfully retrieve all pages.") } if len(pages) == 0 { - t.Error("Exptected to find some pages") + t.Error("Expected to find some pages") } } + +func TestHistory(t *testing.T) { + + repo := Repository{ + directory: "/tmp/.cache/tldr", + remote: "https://tldr.sh/assets/tldr.zip", + ttl: time.Hour * 24 * 7, + } + + repo.makeCacheDir() + + if err2 := repo.RecordHistory("git-pull"); err2 != nil { + t.Error("Expected to record history successful.") + } + + repo.RecordHistory("git-pull") + repo.RecordHistory("git-pull") + repo.RecordHistory("git-push") + repo.RecordHistory("git-push") + repo.RecordHistory("git-fetch") + repo.RecordHistory("git-pull") + + history, err := repo.LoadHistory() + if err != nil { + t.Error("Expected to load history successful.") + } + + length := len(*history) + if length != 3 { + t.Error("Expected to have 3 history records.") + } + + rec1 := HistoryRecord{ + page: "git-push", + count: 2, + } + + if (*history)[0] != rec1 { + t.Errorf("Expected first record to be %+v", rec1) + } + + rec2 := HistoryRecord{ + page: "git-fetch", + count: 1, + } + + if (*history)[1] != rec2 { + t.Errorf("Expected second record to be %+v", rec2) + } + + rec3 := HistoryRecord{ + page: "git-pull", + count: 4, + } + + if (*history)[2] != rec3 { + t.Errorf("Expected third record to be %+v", rec3) + } + +} diff --git a/cmd/tldr/main.go b/cmd/tldr/main.go index 2f99d93..7b2fbda 100644 --- a/cmd/tldr/main.go +++ b/cmd/tldr/main.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "log" + "math" "math/rand" "os" "runtime" @@ -22,6 +23,7 @@ const ( updateUsage = "update local database" versionUsage = "print version and exit" randomUsage = "prints a random page" + historyUsage = "show the latest search history" ) const ( @@ -106,6 +108,11 @@ func printPage(page string) { if err != nil { log.Fatalf("ERROR: writing markdown: %s", err) } + + err2 := repository.RecordHistory(page) + if err2 != nil { + log.Fatalf("ERROR: saving history: %s", err2) + } } func printPageForPlatform(page string, platform string) { @@ -156,6 +163,29 @@ func updatePages() { } } +func printHistory() { + repository, err := cache.NewRepository(remoteURL, ttl) + if err != nil { + log.Fatalf("ERROR: creating cache repository: %s", err) + } + + history, err := repository.LoadHistory() + if err != nil { + log.Fatalf("ERROR: error loading history: %s", err) + } + + hisLen := len(*history) + if hisLen == 0 { + fmt.Println("No history is available yet") + } else { //default print last 10. + size := int(math.Min(10, float64(hisLen))) + for i := 1; i <= size; i++ { + record := (*history)[hisLen-i] + fmt.Printf("%s\n", record) + } + } +} + func main() { version := flag.Bool("version", false, versionUsage) flag.BoolVar(version, "v", false, versionUsage) @@ -176,6 +206,9 @@ func main() { random := flag.Bool("random", false, randomUsage) flag.BoolVar(random, "r", false, randomUsage) + history := flag.Bool("history", false, historyUsage) + flag.BoolVar(history, "t", false, historyUsage) + flag.Parse() if *version { @@ -191,6 +224,8 @@ func main() { printPageForPlatform(page, *platform) } else if *random { printRandomPage() + } else if *history { + printHistory() } else { page := flag.Arg(0) printPage(page)