mirror of
https://github.com/dnote/dnote
synced 2026-03-14 22:45:50 +01:00
Use SQLite (#119)
* Make commands use sqlite * Migrate system * Fix tests to use sqlite * Write test for reducer * Avoid corrupt state in case error occurs in client after server succeeds * Export test functions
This commit is contained in:
parent
0b349a686f
commit
e7940229cf
39 changed files with 3051 additions and 2796 deletions
9
Gopkg.lock
generated
9
Gopkg.lock
generated
|
|
@ -57,6 +57,14 @@
|
|||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bc03901fc8f0965ccba8bc453eae21a9b04f95999eab664c7de6dc7290f4e8f4"
|
||||
name = "github.com/mattn/go-sqlite3"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "25ecb14adfc7543176f7d85291ec7dba82c6f7e4"
|
||||
version = "v1.9.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca"
|
||||
name = "github.com/pkg/errors"
|
||||
|
|
@ -112,6 +120,7 @@
|
|||
"github.com/dnote/actions",
|
||||
"github.com/fatih/color",
|
||||
"github.com/google/go-github/github",
|
||||
"github.com/mattn/go-sqlite3",
|
||||
"github.com/pkg/errors",
|
||||
"github.com/satori/go.uuid",
|
||||
"github.com/spf13/cobra",
|
||||
|
|
|
|||
|
|
@ -44,3 +44,7 @@
|
|||
[[constraint]]
|
||||
name = "github.com/dnote/actions"
|
||||
version = "0.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mattn/go-sqlite3"
|
||||
version = "1.9.0"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
package add
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/cli/core"
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/log"
|
||||
"github.com/dnote/cli/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -28,6 +30,7 @@ func preRun(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// NewCmd returns a new add command
|
||||
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "add <content>",
|
||||
|
|
@ -61,8 +64,7 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
|
|||
}
|
||||
|
||||
ts := time.Now().Unix()
|
||||
note := core.NewNote(content, ts)
|
||||
err := writeNote(ctx, bookName, note, ts)
|
||||
err := writeNote(ctx, bookName, content, ts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write note")
|
||||
}
|
||||
|
|
@ -80,38 +82,45 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func writeNote(ctx infra.DnoteCtx, bookName string, note infra.Note, ts int64) error {
|
||||
dnote, err := core.GetDnote(ctx)
|
||||
func writeNote(ctx infra.DnoteCtx, bookLabel string, content string, ts int64) error {
|
||||
tx, err := ctx.DB.Begin()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get dnote")
|
||||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
|
||||
var book infra.Book
|
||||
|
||||
book, ok := dnote[bookName]
|
||||
if ok {
|
||||
notes := append(dnote[bookName].Notes, note)
|
||||
dnote[bookName] = core.GetUpdatedBook(dnote[bookName], notes)
|
||||
} else {
|
||||
book = core.NewBook(bookName)
|
||||
book.Notes = []infra.Note{note}
|
||||
dnote[bookName] = book
|
||||
|
||||
err = core.LogActionAddBook(ctx, bookName)
|
||||
var bookUUID string
|
||||
err = tx.QueryRow("SELECT uuid FROM books WHERE label = ?", bookLabel).Scan(&bookUUID)
|
||||
if err == sql.ErrNoRows {
|
||||
bookUUID = utils.GenerateUUID()
|
||||
_, err = tx.Exec("INSERT INTO books (uuid, label) VALUES (?, ?)", bookUUID, bookLabel)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to log action")
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "creating the book")
|
||||
}
|
||||
|
||||
err = core.LogActionAddBook(tx, bookLabel)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "logging action")
|
||||
}
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "finding the book")
|
||||
}
|
||||
|
||||
err = core.LogActionAddNote(ctx, note.UUID, book.Name, note.Content, ts)
|
||||
noteUUID := utils.GenerateUUID()
|
||||
_, err = tx.Exec(`INSERT INTO notes (uuid, book_uuid, content, added_on, public)
|
||||
VALUES (?, ?, ?, ?, ?);`, noteUUID, bookUUID, content, ts, false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to log action")
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "creating the note")
|
||||
}
|
||||
err = core.LogActionAddNote(tx, noteUUID, bookLabel, content, ts)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "logging action")
|
||||
}
|
||||
|
||||
err = core.WriteDnote(ctx, dnote)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write to dnote file")
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package cat
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/cli/core"
|
||||
|
|
@ -30,6 +30,7 @@ func preRun(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// NewCmd returns a new cat command
|
||||
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cat <book name> <note index>",
|
||||
|
|
@ -44,30 +45,48 @@ func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
type noteInfo struct {
|
||||
BookLabel string
|
||||
UUID string
|
||||
Content string
|
||||
AddedOn int64
|
||||
EditedOn int64
|
||||
}
|
||||
|
||||
func NewRun(ctx infra.DnoteCtx) core.RunEFunc {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
dnote, err := core.GetDnote(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading dnote")
|
||||
db := ctx.DB
|
||||
bookLabel := args[0]
|
||||
noteID := args[1]
|
||||
|
||||
var bookUUID string
|
||||
err := db.QueryRow("SELECT uuid FROM books WHERE label = ?", bookLabel).Scan(&bookUUID)
|
||||
if err == sql.ErrNoRows {
|
||||
return errors.Errorf("book '%s' not found", bookLabel)
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "querying the book")
|
||||
}
|
||||
|
||||
bookName := args[0]
|
||||
noteIdx, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "parsing note index '%+v'", args[1])
|
||||
var info noteInfo
|
||||
err = db.QueryRow(`SELECT books.label, notes.uuid, notes.content, notes.added_on, notes.edited_on
|
||||
FROM notes
|
||||
INNER JOIN books ON books.uuid = notes.book_uuid
|
||||
WHERE notes.id = ? AND books.uuid = ?`, noteID, bookUUID).
|
||||
Scan(&info.BookLabel, &info.UUID, &info.Content, &info.AddedOn, &info.EditedOn)
|
||||
if err == sql.ErrNoRows {
|
||||
return errors.Errorf("note %s not found in the book '%s'", noteID, bookLabel)
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "querying the note")
|
||||
}
|
||||
|
||||
book := dnote[bookName]
|
||||
note := book.Notes[noteIdx]
|
||||
|
||||
log.Infof("book name: %s\n", bookName)
|
||||
log.Infof("note uuid: %s\n", note.UUID)
|
||||
log.Infof("created at: %s\n", time.Unix(note.AddedOn, 0).Format("Jan 2, 2006 3:04pm (MST)"))
|
||||
if note.EditedOn != 0 {
|
||||
log.Infof("updated at: %s\n", time.Unix(note.EditedOn, 0).Format("Jan 2, 2006 3:04pm (MST)"))
|
||||
log.Infof("book name: %s\n", info.BookLabel)
|
||||
log.Infof("note uuid: %s\n", info.UUID)
|
||||
log.Infof("created at: %s\n", time.Unix(info.AddedOn, 0).Format("Jan 2, 2006 3:04pm (MST)"))
|
||||
if info.EditedOn != 0 {
|
||||
log.Infof("updated at: %s\n", time.Unix(info.EditedOn, 0).Format("Jan 2, 2006 3:04pm (MST)"))
|
||||
}
|
||||
fmt.Printf("\n------------------------content------------------------\n")
|
||||
fmt.Printf("%s", note.Content)
|
||||
fmt.Printf("%s", info.Content)
|
||||
fmt.Printf("\n-------------------------------------------------------\n")
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package edit
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/cli/core"
|
||||
|
|
@ -21,6 +21,7 @@ var example = `
|
|||
* Skip the prompt by providing new content directly
|
||||
dnote edit js 3 -c "new content"`
|
||||
|
||||
// NewCmd returns a new edit command
|
||||
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "edit",
|
||||
|
|
@ -47,62 +48,64 @@ func preRun(cmd *cobra.Command, args []string) error {
|
|||
|
||||
func newRun(ctx infra.DnoteCtx) core.RunEFunc {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
dnote, err := core.GetDnote(ctx)
|
||||
db := ctx.DB
|
||||
bookLabel := args[0]
|
||||
noteID := args[1]
|
||||
|
||||
bookUUID, err := core.GetBookUUID(ctx, bookLabel)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read dnote")
|
||||
return errors.Wrap(err, "finding book uuid")
|
||||
}
|
||||
|
||||
targetBookName := args[0]
|
||||
targetIdx, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to parse the given index %+v", args[1])
|
||||
var noteUUID, oldContent string
|
||||
err = db.QueryRow("SELECT uuid, content FROM notes WHERE id = ? AND book_uuid = ?", noteID, bookUUID).Scan(¬eUUID, &oldContent)
|
||||
if err == sql.ErrNoRows {
|
||||
return errors.Errorf("note %s not found in the book '%s'", noteID, bookLabel)
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "querying the book")
|
||||
}
|
||||
|
||||
targetBook, exists := dnote[targetBookName]
|
||||
if !exists {
|
||||
return errors.Errorf("Book %s does not exist", targetBookName)
|
||||
}
|
||||
if targetIdx > len(targetBook.Notes)-1 {
|
||||
return errors.Errorf("Book %s does not have note with index %d", targetBookName, targetIdx)
|
||||
}
|
||||
targetNote := targetBook.Notes[targetIdx]
|
||||
|
||||
if newContent == "" {
|
||||
fpath := core.GetDnoteTmpContentPath(ctx)
|
||||
|
||||
e := ioutil.WriteFile(fpath, []byte(targetNote.Content), 0644)
|
||||
e := ioutil.WriteFile(fpath, []byte(oldContent), 0644)
|
||||
if e != nil {
|
||||
return errors.Wrap(e, "Failed to prepare editor content")
|
||||
return errors.Wrap(e, "preparing tmp content file")
|
||||
}
|
||||
|
||||
e = core.GetEditorInput(ctx, fpath, &newContent)
|
||||
if e != nil {
|
||||
return errors.Wrap(err, "Failed to get editor input")
|
||||
return errors.Wrap(err, "getting editor input")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if targetNote.Content == newContent {
|
||||
if oldContent == newContent {
|
||||
return errors.New("Nothing changed")
|
||||
}
|
||||
|
||||
ts := time.Now().Unix()
|
||||
newContent = core.SanitizeContent(newContent)
|
||||
|
||||
targetNote.Content = core.SanitizeContent(newContent)
|
||||
targetNote.EditedOn = ts
|
||||
targetBook.Notes[targetIdx] = targetNote
|
||||
dnote[targetBookName] = targetBook
|
||||
|
||||
err = core.LogActionEditNote(ctx, targetNote.UUID, targetBook.Name, targetNote.Content, ts)
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to log action")
|
||||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
_, err = tx.Exec(`UPDATE notes
|
||||
SET content = ?, edited_on = ?
|
||||
WHERE id = ? AND book_uuid = ?`, newContent, ts, noteID, bookUUID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "updating the note")
|
||||
}
|
||||
|
||||
err = core.WriteDnote(ctx, dnote)
|
||||
err = core.LogActionEditNote(tx, noteUUID, bookLabel, newContent, ts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write dnote")
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "logging an action")
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
log.Printf("new content: %s\n", newContent)
|
||||
log.Success("edited the note\n")
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
var example = `
|
||||
dnote login`
|
||||
|
||||
// NewCmd returns a new login command
|
||||
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "login",
|
||||
|
|
|
|||
97
cmd/ls/ls.go
97
cmd/ls/ls.go
|
|
@ -1,8 +1,8 @@
|
|||
package ls
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/dnote/cli/core"
|
||||
|
|
@ -33,6 +33,7 @@ func preRun(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// NewCmd returns a new ls command
|
||||
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls <book name?>",
|
||||
|
|
@ -49,22 +50,17 @@ func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
|
|||
|
||||
func NewRun(ctx infra.DnoteCtx) core.RunEFunc {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
dnote, err := core.GetDnote(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read dnote")
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
if err := printBooks(dnote); err != nil {
|
||||
return errors.Wrap(err, "Failed to print books")
|
||||
if err := printBooks(ctx); err != nil {
|
||||
return errors.Wrap(err, "viewing books")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
bookName := args[0]
|
||||
if err := printNotes(dnote, bookName); err != nil {
|
||||
return errors.Wrapf(err, "Failed to print notes for the book %s", bookName)
|
||||
if err := printNotes(ctx, bookName); err != nil {
|
||||
return errors.Wrapf(err, "viewing book '%s'", bookName)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -73,18 +69,14 @@ func NewRun(ctx infra.DnoteCtx) core.RunEFunc {
|
|||
|
||||
// bookInfo is an information about the book to be printed on screen
|
||||
type bookInfo struct {
|
||||
BookName string
|
||||
BookLabel string
|
||||
NoteCount int
|
||||
}
|
||||
|
||||
func getBookInfos(dnote infra.Dnote) []bookInfo {
|
||||
var ret []bookInfo
|
||||
|
||||
for bookName, book := range dnote {
|
||||
ret = append(ret, bookInfo{BookName: bookName, NoteCount: len(book.Notes)})
|
||||
}
|
||||
|
||||
return ret
|
||||
// noteInfo is an information about the note to be printed on screen
|
||||
type noteInfo struct {
|
||||
ID int
|
||||
Content string
|
||||
}
|
||||
|
||||
// getNewlineIdx returns the index of newline character in a string
|
||||
|
|
@ -114,30 +106,71 @@ func formatContent(noteContent string) (string, bool) {
|
|||
return strings.Trim(noteContent, " "), false
|
||||
}
|
||||
|
||||
func printBooks(dnote infra.Dnote) error {
|
||||
infos := getBookInfos(dnote)
|
||||
func printBooks(ctx infra.DnoteCtx) error {
|
||||
db := ctx.DB
|
||||
|
||||
// Show books with more notes first
|
||||
sort.SliceStable(infos, func(i, j int) bool {
|
||||
return infos[i].NoteCount > infos[j].NoteCount
|
||||
})
|
||||
rows, err := db.Query(`SELECT books.label, count(notes.uuid) note_count
|
||||
FROM books
|
||||
INNER JOIN notes ON notes.book_uuid = books.uuid
|
||||
GROUP BY books.uuid
|
||||
ORDER BY books.label ASC;`)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "querying books")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
infos := []bookInfo{}
|
||||
for rows.Next() {
|
||||
var info bookInfo
|
||||
err = rows.Scan(&info.BookLabel, &info.NoteCount)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "scanning a row")
|
||||
}
|
||||
|
||||
infos = append(infos, info)
|
||||
}
|
||||
|
||||
for _, info := range infos {
|
||||
log.Printf("%s %s\n", info.BookName, log.SprintfYellow("(%d)", info.NoteCount))
|
||||
log.Printf("%s %s\n", info.BookLabel, log.SprintfYellow("(%d)", info.NoteCount))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printNotes(dnote infra.Dnote, bookName string) error {
|
||||
func printNotes(ctx infra.DnoteCtx, bookName string) error {
|
||||
db := ctx.DB
|
||||
|
||||
var bookUUID string
|
||||
err := db.QueryRow("SELECT uuid FROM books WHERE label = ?", bookName).Scan(&bookUUID)
|
||||
if err == sql.ErrNoRows {
|
||||
return errors.New("book not found")
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "querying the book")
|
||||
}
|
||||
|
||||
rows, err := db.Query(`SELECT id, content FROM notes WHERE book_uuid = ? ORDER BY added_on ASC;`, bookUUID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "querying notes")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
infos := []noteInfo{}
|
||||
for rows.Next() {
|
||||
var info noteInfo
|
||||
err = rows.Scan(&info.ID, &info.Content)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "scanning a row")
|
||||
}
|
||||
|
||||
infos = append(infos, info)
|
||||
}
|
||||
|
||||
log.Infof("on book %s\n", bookName)
|
||||
|
||||
book := dnote[bookName]
|
||||
for _, info := range infos {
|
||||
content, isExcerpt := formatContent(info.Content)
|
||||
|
||||
for i, note := range book.Notes {
|
||||
content, isExcerpt := formatContent(note.Content)
|
||||
|
||||
index := log.SprintfYellow("(%d)", i)
|
||||
index := log.SprintfYellow("(%d)", info.ID)
|
||||
if isExcerpt {
|
||||
content = fmt.Sprintf("%s %s", content, log.SprintfYellow("[---More---]"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package remove
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/dnote/cli/core"
|
||||
"github.com/dnote/cli/infra"
|
||||
|
|
@ -39,111 +39,109 @@ func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
|
|||
func newRun(ctx infra.DnoteCtx) core.RunEFunc {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
if targetBookName != "" {
|
||||
err := book(ctx, targetBookName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to delete the book")
|
||||
}
|
||||
} else {
|
||||
if len(args) < 2 {
|
||||
return errors.New("Missing argument")
|
||||
if err := removeBook(ctx, targetBookName); err != nil {
|
||||
return errors.Wrap(err, "removing the book")
|
||||
}
|
||||
|
||||
targetBook := args[0]
|
||||
noteIndex, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err = note(ctx, noteIndex, targetBook)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to delete the note")
|
||||
}
|
||||
if len(args) < 2 {
|
||||
return errors.New("Missing argument")
|
||||
}
|
||||
|
||||
targetBook := args[0]
|
||||
noteID := args[1]
|
||||
|
||||
if err := removeNote(ctx, noteID, targetBook); err != nil {
|
||||
return errors.Wrap(err, "removing the note")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// note deletes the note in a certain index.
|
||||
func note(ctx infra.DnoteCtx, index int, bookName string) error {
|
||||
dnote, err := core.GetDnote(ctx)
|
||||
func removeNote(ctx infra.DnoteCtx, noteID, bookLabel string) error {
|
||||
db := ctx.DB
|
||||
|
||||
bookUUID, err := core.GetBookUUID(ctx, bookLabel)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get dnote")
|
||||
return errors.Wrap(err, "finding book uuid")
|
||||
}
|
||||
|
||||
book, exists := dnote[bookName]
|
||||
if !exists {
|
||||
return errors.Errorf("Book with the name '%s' does not exist", bookName)
|
||||
}
|
||||
notes := book.Notes
|
||||
|
||||
if len(notes)-1 < index {
|
||||
fmt.Println("Error : The note with that index is not found.")
|
||||
return nil
|
||||
var noteUUID, noteContent string
|
||||
err = db.QueryRow("SELECT uuid, content FROM notes WHERE id = ? AND book_uuid = ?", noteID, bookUUID).Scan(¬eUUID, ¬eContent)
|
||||
if err == sql.ErrNoRows {
|
||||
return errors.Errorf("note %s not found in the book '%s'", noteID, bookLabel)
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "querying the book")
|
||||
}
|
||||
|
||||
content := notes[index].Content
|
||||
log.Printf("content: \"%s\"\n", content)
|
||||
// todo: multiline
|
||||
log.Printf("content: \"%s\"\n", noteContent)
|
||||
|
||||
ok, err := utils.AskConfirmation("remove this note?", false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get confirmation")
|
||||
return errors.Wrap(err, "getting confirmation")
|
||||
}
|
||||
if !ok {
|
||||
log.Warnf("aborted by user\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
note := notes[index]
|
||||
dnote[bookName] = core.GetUpdatedBook(dnote[bookName], append(notes[:index], notes[index+1:]...))
|
||||
|
||||
err = core.LogActionRemoveNote(ctx, note.UUID, book.Name)
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to log action")
|
||||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
|
||||
err = core.WriteDnote(ctx, dnote)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write dnote")
|
||||
if _, err = tx.Exec("DELETE FROM notes WHERE uuid = ? AND book_uuid = ?", noteUUID, bookUUID); err != nil {
|
||||
return errors.Wrap(err, "removing the note")
|
||||
}
|
||||
if err = core.LogActionRemoveNote(tx, noteUUID, bookLabel); err != nil {
|
||||
return errors.Wrap(err, "logging the remove_note action")
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
log.Successf("removed from %s\n", bookLabel)
|
||||
|
||||
log.Successf("removed from %s\n", bookName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// book deletes a book with the given name
|
||||
func book(ctx infra.DnoteCtx, bookName string) error {
|
||||
ok, err := utils.AskConfirmation(fmt.Sprintf("delete book '%s' and all its notes?", bookName), false)
|
||||
func removeBook(ctx infra.DnoteCtx, bookLabel string) error {
|
||||
db := ctx.DB
|
||||
|
||||
bookUUID, err := core.GetBookUUID(ctx, bookLabel)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "finding book uuid")
|
||||
}
|
||||
|
||||
ok, err := utils.AskConfirmation(fmt.Sprintf("delete book '%s' and all its notes?", bookLabel), false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting confirmation")
|
||||
}
|
||||
if !ok {
|
||||
log.Warnf("aborted by user\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
dnote, err := core.GetDnote(ctx)
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
|
||||
for n, book := range dnote {
|
||||
if n == bookName {
|
||||
delete(dnote, n)
|
||||
|
||||
err = core.LogActionRemoveBook(ctx, book.Name)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to log action")
|
||||
}
|
||||
err := core.WriteDnote(ctx, dnote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Success("removed book\n")
|
||||
return nil
|
||||
}
|
||||
if _, err = tx.Exec("DELETE FROM notes WHERE book_uuid = ?", bookUUID); err != nil {
|
||||
return errors.Wrap(err, "removing notes in the book")
|
||||
}
|
||||
if _, err = tx.Exec("DELETE FROM books WHERE uuid = ?", bookUUID); err != nil {
|
||||
return errors.Wrap(err, "removing the book")
|
||||
}
|
||||
if err = core.LogActionRemoveBook(tx, bookLabel); err != nil {
|
||||
return errors.Wrap(err, "loging the remove_book action")
|
||||
}
|
||||
|
||||
return errors.Errorf("Book '%s' was not found", bookName)
|
||||
tx.Commit()
|
||||
|
||||
log.Success("removed book\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,18 +27,21 @@ func Execute() error {
|
|||
|
||||
// Prepare initializes necessary files
|
||||
func Prepare(ctx infra.DnoteCtx) error {
|
||||
err := core.MigrateToDnoteDir(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "initializing dnote dir")
|
||||
if err := core.InitFiles(ctx); err != nil {
|
||||
return errors.Wrap(err, "initializing files")
|
||||
}
|
||||
|
||||
err = core.InitFiles(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "initiating files")
|
||||
if err := infra.InitDB(ctx); err != nil {
|
||||
return errors.Wrap(err, "initializing database")
|
||||
}
|
||||
if err := infra.InitSystem(ctx); err != nil {
|
||||
return errors.Wrap(err, "initializing system data")
|
||||
}
|
||||
|
||||
err = migrate.Migrate(ctx)
|
||||
if err != nil {
|
||||
if err := migrate.Legacy(ctx); err != nil {
|
||||
return errors.Wrap(err, "running legacy migration")
|
||||
}
|
||||
if err := migrate.Run(ctx); err != nil {
|
||||
return errors.Wrap(err, "running migration")
|
||||
}
|
||||
|
||||
|
|
|
|||
120
cmd/sync/sync.go
120
cmd/sync/sync.go
|
|
@ -3,6 +3,7 @@ package sync
|
|||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -20,6 +21,7 @@ import (
|
|||
var example = `
|
||||
dnote sync`
|
||||
|
||||
// NewCmd returns a new sync command
|
||||
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "sync",
|
||||
|
|
@ -44,38 +46,42 @@ type syncPayload struct {
|
|||
|
||||
func newRun(ctx infra.DnoteCtx) core.RunEFunc {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
db := ctx.DB
|
||||
|
||||
config, err := core.ReadConfig(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the config")
|
||||
return errors.Wrap(err, "reading the config")
|
||||
}
|
||||
timestamp, err := core.ReadTimestamp(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the timestamp")
|
||||
}
|
||||
actions, err := core.ReadActionLog(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the action log")
|
||||
}
|
||||
|
||||
if config.APIKey == "" {
|
||||
log.Error("login required. please run `dnote login`\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
payload, err := getPayload(actions, timestamp)
|
||||
var bookmark int
|
||||
err = db.QueryRow("SELECT value FROM system WHERE key = ?", "bookmark").Scan(&bookmark)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get dnote payload")
|
||||
return errors.Wrap(err, "getting bookmark")
|
||||
}
|
||||
|
||||
actions, err := getLocalActions(db)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting local actions")
|
||||
}
|
||||
|
||||
payload, err := newPayload(actions, bookmark)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting the request payload")
|
||||
}
|
||||
|
||||
log.Infof("writing changes (total %d).", len(actions))
|
||||
resp, err := postActions(ctx, config.APIKey, payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to post to the server ")
|
||||
return errors.Wrap(err, "posting to the server")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read failed response body")
|
||||
return errors.Wrap(err, "reading the response body")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
|
|
@ -88,34 +94,36 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
|
|||
fmt.Println(" done.")
|
||||
|
||||
var respData responseData
|
||||
err = json.Unmarshal(body, &respData)
|
||||
if err = json.Unmarshal(body, &respData); err != nil {
|
||||
return errors.Wrap(err, "unmarshalling the payload")
|
||||
}
|
||||
|
||||
// First, remove our actions because server has successfully ingested them
|
||||
if _, err = db.Exec("DELETE FROM actions"); err != nil {
|
||||
return errors.Wrap(err, "deleting actions")
|
||||
}
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to unmarshal payload")
|
||||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
|
||||
log.Infof("resolving delta (total %d).", len(respData.Actions))
|
||||
err = core.ReduceAll(ctx, respData.Actions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to reduce returned actions")
|
||||
if err := core.ReduceAll(ctx, tx, respData.Actions); err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "reducing returned actions")
|
||||
}
|
||||
|
||||
if _, err = tx.Exec("UPDATE system SET value = ? WHERE key = ?", respData.Bookmark, "bookmark"); err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "updating the bookmark")
|
||||
}
|
||||
|
||||
fmt.Println(" done.")
|
||||
|
||||
// Update bookmark
|
||||
ts, err := core.ReadTimestamp(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the timestamp")
|
||||
}
|
||||
ts.Bookmark = respData.Bookmark
|
||||
|
||||
err = core.WriteTimestamp(ctx, ts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to update bookmark")
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
log.Success("success\n")
|
||||
if err := core.ClearActionLog(ctx); err != nil {
|
||||
return errors.Wrap(err, "Failed to clear the action log")
|
||||
}
|
||||
|
||||
if err := core.CheckUpdate(ctx); err != nil {
|
||||
log.Error(errors.Wrap(err, "automatically checking updates").Error())
|
||||
|
|
@ -125,20 +133,20 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func getPayload(actions []actions.Action, timestamp infra.Timestamp) (*bytes.Buffer, error) {
|
||||
func newPayload(actions []actions.Action, bookmark int) (*bytes.Buffer, error) {
|
||||
compressedActions, err := compressActions(actions)
|
||||
if err != nil {
|
||||
return &bytes.Buffer{}, errors.Wrap(err, "Failed to compress actions")
|
||||
return &bytes.Buffer{}, errors.Wrap(err, "compressing actions")
|
||||
}
|
||||
|
||||
payload := syncPayload{
|
||||
Bookmark: timestamp.Bookmark,
|
||||
Bookmark: bookmark,
|
||||
Actions: compressedActions,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return &bytes.Buffer{}, errors.Wrap(err, "Failed to marshal paylaod into JSON")
|
||||
return &bytes.Buffer{}, errors.Wrap(err, "marshalling paylaod into JSON")
|
||||
}
|
||||
|
||||
ret := bytes.NewBuffer(b)
|
||||
|
|
@ -148,7 +156,7 @@ func getPayload(actions []actions.Action, timestamp infra.Timestamp) (*bytes.Buf
|
|||
func compressActions(actions []actions.Action) ([]byte, error) {
|
||||
b, err := json.Marshal(&actions)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal actions into JSON")
|
||||
return nil, errors.Wrap(err, "marshalling actions into JSON")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
|
@ -156,11 +164,11 @@ func compressActions(actions []actions.Action) ([]byte, error) {
|
|||
|
||||
_, err = g.Write(b)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to write to gzip writer")
|
||||
return nil, errors.Wrap(err, "writing to gzip writer")
|
||||
}
|
||||
|
||||
if err = g.Close(); err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to close gzip writer")
|
||||
return nil, errors.Wrap(err, "closing gzip writer")
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
|
|
@ -170,7 +178,7 @@ func postActions(ctx infra.DnoteCtx, APIKey string, payload io.Reader) (*http.Re
|
|||
endpoint := fmt.Sprintf("%s/v1/sync", ctx.APIEndpoint)
|
||||
req, err := http.NewRequest("POST", endpoint, payload)
|
||||
if err != nil {
|
||||
return &http.Response{}, errors.Wrap(err, "Failed to construct HTTP request")
|
||||
return &http.Response{}, errors.Wrap(err, "forming an HTTP request")
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", APIKey)
|
||||
|
|
@ -179,8 +187,36 @@ func postActions(ctx infra.DnoteCtx, APIKey string, payload io.Reader) (*http.Re
|
|||
client := http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return &http.Response{}, errors.Wrap(err, "Failed to make request")
|
||||
return &http.Response{}, errors.Wrap(err, "making a request")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func getLocalActions(db *sql.DB) ([]actions.Action, error) {
|
||||
ret := []actions.Action{}
|
||||
|
||||
rows, err := db.Query("SELECT uuid, schema, type, data, timestamp FROM actions")
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "querying actions")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var action actions.Action
|
||||
|
||||
err = rows.Scan(&action.UUID, &action.Schema, &action.Type, &action.Data, &action.Timestamp)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "scanning a row")
|
||||
}
|
||||
|
||||
ret = append(ret, action)
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "scanning rows")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/actions"
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
func LogActionAddNote(ctx infra.DnoteCtx, noteUUID, bookName, content string, timestamp int64) error {
|
||||
// LogActionAddNote logs an action for adding a note
|
||||
func LogActionAddNote(tx *sql.Tx, noteUUID, bookName, content string, timestamp int64) error {
|
||||
b, err := json.Marshal(actions.AddNoteDataV2{
|
||||
NoteUUID: noteUUID,
|
||||
BookName: bookName,
|
||||
|
|
@ -19,113 +19,79 @@ func LogActionAddNote(ctx infra.DnoteCtx, noteUUID, bookName, content string, ti
|
|||
Public: false,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal data into JSON")
|
||||
return errors.Wrap(err, "marshalling data into JSON")
|
||||
}
|
||||
|
||||
action := actions.Action{
|
||||
UUID: uuid.NewV4().String(),
|
||||
Schema: 2,
|
||||
Type: actions.ActionAddNote,
|
||||
Data: b,
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
|
||||
if err := LogAction(ctx, action); err != nil {
|
||||
return errors.Wrapf(err, "Failed to log action type %s", actions.ActionAddNote)
|
||||
if err := LogAction(tx, 2, actions.ActionAddNote, string(b), timestamp); err != nil {
|
||||
return errors.Wrapf(err, "logging action")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LogActionRemoveNote(ctx infra.DnoteCtx, noteUUID, bookName string) error {
|
||||
// LogActionRemoveNote logs an action for removing a book
|
||||
func LogActionRemoveNote(tx *sql.Tx, noteUUID, bookName string) error {
|
||||
b, err := json.Marshal(actions.RemoveNoteDataV1{
|
||||
NoteUUID: noteUUID,
|
||||
BookName: bookName,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal data into JSON")
|
||||
return errors.Wrap(err, "marshalling data into JSON")
|
||||
}
|
||||
|
||||
action := actions.Action{
|
||||
UUID: uuid.NewV4().String(),
|
||||
Schema: 1,
|
||||
Type: actions.ActionRemoveNote,
|
||||
Data: b,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := LogAction(ctx, action); err != nil {
|
||||
return errors.Wrapf(err, "Failed to log action type %s", actions.ActionRemoveNote)
|
||||
ts := time.Now().Unix()
|
||||
if err := LogAction(tx, 1, actions.ActionRemoveNote, string(b), ts); err != nil {
|
||||
return errors.Wrapf(err, "logging action")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LogActionEditNote(ctx infra.DnoteCtx, noteUUID, bookName, content string, ts int64) error {
|
||||
// LogActionEditNote logs an action for editing a note
|
||||
func LogActionEditNote(tx *sql.Tx, noteUUID, bookName, content string, ts int64) error {
|
||||
b, err := json.Marshal(actions.EditNoteDataV2{
|
||||
NoteUUID: noteUUID,
|
||||
FromBook: bookName,
|
||||
Content: &content,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal data into JSON")
|
||||
return errors.Wrap(err, "marshalling data into JSON")
|
||||
}
|
||||
|
||||
action := actions.Action{
|
||||
UUID: uuid.NewV4().String(),
|
||||
Schema: 2,
|
||||
Type: actions.ActionEditNote,
|
||||
Data: b,
|
||||
Timestamp: ts,
|
||||
}
|
||||
|
||||
if err := LogAction(ctx, action); err != nil {
|
||||
return errors.Wrapf(err, "Failed to log action type %s", actions.ActionEditNote)
|
||||
if err := LogAction(tx, 2, actions.ActionEditNote, string(b), ts); err != nil {
|
||||
return errors.Wrapf(err, "logging action")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LogActionAddBook(ctx infra.DnoteCtx, name string) error {
|
||||
// LogActionAddBook logs an action for adding a book
|
||||
func LogActionAddBook(tx *sql.Tx, name string) error {
|
||||
b, err := json.Marshal(actions.AddBookDataV1{
|
||||
BookName: name,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal data into JSON")
|
||||
return errors.Wrap(err, "marshalling data into JSON")
|
||||
}
|
||||
|
||||
action := actions.Action{
|
||||
UUID: uuid.NewV4().String(),
|
||||
Schema: 1,
|
||||
Type: actions.ActionAddBook,
|
||||
Data: b,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := LogAction(ctx, action); err != nil {
|
||||
return errors.Wrapf(err, "Failed to log action type %s", actions.ActionAddBook)
|
||||
ts := time.Now().Unix()
|
||||
if err := LogAction(tx, 1, actions.ActionAddBook, string(b), ts); err != nil {
|
||||
return errors.Wrapf(err, "logging action")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LogActionRemoveBook(ctx infra.DnoteCtx, name string) error {
|
||||
// LogActionRemoveBook logs an action for removing book
|
||||
func LogActionRemoveBook(tx *sql.Tx, name string) error {
|
||||
b, err := json.Marshal(actions.RemoveBookDataV1{BookName: name})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal data into JSON")
|
||||
return errors.Wrap(err, "marshalling data into JSON")
|
||||
}
|
||||
|
||||
action := actions.Action{
|
||||
UUID: uuid.NewV4().String(),
|
||||
Schema: 1,
|
||||
Type: actions.ActionRemoveBook,
|
||||
Data: b,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := LogAction(ctx, action); err != nil {
|
||||
return errors.Wrapf(err, "Failed to log action type %s", actions.ActionRemoveBook)
|
||||
ts := time.Now().Unix()
|
||||
if err := LogAction(tx, 1, actions.ActionRemoveBook, string(b), ts); err != nil {
|
||||
return errors.Wrapf(err, "logging action")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -11,34 +11,44 @@ import (
|
|||
|
||||
func TestLogActionEditNote(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
|
||||
InitFiles(ctx)
|
||||
// Execute
|
||||
db := ctx.DB
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
|
||||
if err := LogActionEditNote(ctx, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "js", "updated content", 1536168581); err != nil {
|
||||
if err := LogActionEditNote(tx, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "js", "updated content", 1536168581); err != nil {
|
||||
t.Fatalf("Failed to perform %s", err.Error())
|
||||
}
|
||||
|
||||
b := testutils.ReadFile(ctx, "actions")
|
||||
var got []actions.Action
|
||||
tx.Commit()
|
||||
|
||||
if err := json.Unmarshal(b, &got); err != nil {
|
||||
panic(errors.Wrap(err, "unmarshalling actions"))
|
||||
// Test
|
||||
var actionCount int
|
||||
if err := db.QueryRow("SELECT count(*) FROM actions;").Scan(&actionCount); err != nil {
|
||||
panic(errors.Wrap(err, "counting actions"))
|
||||
}
|
||||
var action actions.Action
|
||||
if err := db.QueryRow("SELECT uuid, schema, type, timestamp, data FROM actions").
|
||||
Scan(&action.UUID, &action.Schema, &action.Type, &action.Timestamp, &action.Data); err != nil {
|
||||
panic(errors.Wrap(err, "querying action"))
|
||||
}
|
||||
|
||||
var actionData actions.EditNoteDataV2
|
||||
if err := json.Unmarshal(got[0].Data, &actionData); err != nil {
|
||||
if err := json.Unmarshal(action.Data, &actionData); err != nil {
|
||||
panic(errors.Wrap(err, "unmarshalling action data"))
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, len(got), 1, "action length mismatch")
|
||||
testutils.AssertNotEqual(t, got[0].UUID, "", "action uuid mismatch")
|
||||
testutils.AssertEqual(t, got[0].Schema, 2, "action schema mismatch")
|
||||
testutils.AssertEqual(t, got[0].Type, actions.ActionEditNote, "action type mismatch")
|
||||
testutils.AssertNotEqual(t, got[0].Timestamp, 0, "action timestamp mismatch")
|
||||
if actionCount != 1 {
|
||||
t.Fatalf("action count mismatch. got %d", actionCount)
|
||||
}
|
||||
testutils.AssertNotEqual(t, action.UUID, "", "action uuid mismatch")
|
||||
testutils.AssertEqual(t, action.Schema, 2, "action schema mismatch")
|
||||
testutils.AssertEqual(t, action.Type, actions.ActionEditNote, "action type mismatch")
|
||||
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuid mismatch")
|
||||
testutils.AssertEqual(t, actionData.FromBook, "js", "action data from_book mismatch")
|
||||
testutils.AssertEqual(t, *actionData.Content, "updated content", "action data content mismatch")
|
||||
|
|
|
|||
436
core/core.go
436
core/core.go
|
|
@ -1,34 +1,29 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/actions"
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/migrate"
|
||||
"github.com/dnote/cli/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/satori/go.uuid"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// TimestampFilename is the name of the file containing upgrade info
|
||||
TimestampFilename = "timestamps"
|
||||
// DnoteDirName is the name of the directory containing dnote files
|
||||
DnoteDirName = ".dnote"
|
||||
ConfigFilename = "dnoterc"
|
||||
DnoteFilename = "dnote"
|
||||
ActionFilename = "actions"
|
||||
// ConfigFilename is the name of the config file
|
||||
ConfigFilename = "dnoterc"
|
||||
// TmpContentFilename is the name of the temporary file that holds editor input
|
||||
TmpContentFilename = "DNOTE_TMPCONTENT"
|
||||
)
|
||||
|
||||
// RunEFunc is a function type of dnote commands
|
||||
type RunEFunc func(*cobra.Command, []string) error
|
||||
|
||||
// GetConfigPath returns the path to the dnote config file
|
||||
|
|
@ -36,45 +31,29 @@ func GetConfigPath(ctx infra.DnoteCtx) string {
|
|||
return fmt.Sprintf("%s/%s", ctx.DnoteDir, ConfigFilename)
|
||||
}
|
||||
|
||||
// GetDnotePath returns the path to the dnote file
|
||||
func GetDnotePath(ctx infra.DnoteCtx) string {
|
||||
return fmt.Sprintf("%s/%s", ctx.DnoteDir, DnoteFilename)
|
||||
}
|
||||
|
||||
// GetTimestampPath returns the path to the file containing dnote upgrade
|
||||
// information
|
||||
func GetTimestampPath(ctx infra.DnoteCtx) string {
|
||||
return fmt.Sprintf("%s/%s", ctx.DnoteDir, TimestampFilename)
|
||||
}
|
||||
|
||||
// GetActionPath returns the path to the file containing user actions
|
||||
func GetActionPath(ctx infra.DnoteCtx) string {
|
||||
return fmt.Sprintf("%s/%s", ctx.DnoteDir, ActionFilename)
|
||||
}
|
||||
|
||||
// GetDnoteTmpContentPath returns the path to the temporary file containing
|
||||
// content being added or edited
|
||||
func GetDnoteTmpContentPath(ctx infra.DnoteCtx) string {
|
||||
return fmt.Sprintf("%s/%s", ctx.DnoteDir, TmpContentFilename)
|
||||
}
|
||||
|
||||
// initActionFile populates action file if it does not exist
|
||||
func initActionFile(ctx infra.DnoteCtx) error {
|
||||
path := GetActionPath(ctx)
|
||||
// GetBookUUID returns a uuid of a book given a label
|
||||
func GetBookUUID(ctx infra.DnoteCtx, label string) (string, error) {
|
||||
db := ctx.DB
|
||||
|
||||
if utils.FileExists(path) {
|
||||
return nil
|
||||
var ret string
|
||||
err := db.QueryRow("SELECT uuid FROM books WHERE label = ?", label).Scan(&ret)
|
||||
if err == sql.ErrNoRows {
|
||||
return ret, errors.Errorf("book '%s' not found", label)
|
||||
} else if err != nil {
|
||||
return ret, errors.Wrap(err, "querying the book")
|
||||
}
|
||||
|
||||
b, err := json.Marshal(&[]actions.Action{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get initial action content")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(path, b, 0644)
|
||||
return err
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// getEditorCommand returns the system's editor command with appropriate flags,
|
||||
// if necessary, to make the command wait until editor is close to exit.
|
||||
func getEditorCommand() string {
|
||||
editor := os.Getenv("EDITOR")
|
||||
|
||||
|
|
@ -100,34 +79,11 @@ func getEditorCommand() string {
|
|||
|
||||
// InitFiles creates, if necessary, the dnote directory and files inside
|
||||
func InitFiles(ctx infra.DnoteCtx) error {
|
||||
fresh, err := isFreshInstall(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to check if fresh install")
|
||||
if err := initDnoteDir(ctx); err != nil {
|
||||
return errors.Wrap(err, "creating the dnote dir")
|
||||
}
|
||||
|
||||
err = initDnoteDir(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create dnote dir")
|
||||
}
|
||||
err = initConfigFile(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to generate config file")
|
||||
}
|
||||
err = initDnoteFile(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create dnote file")
|
||||
}
|
||||
err = initTimestampFile(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create dnote upgrade file")
|
||||
}
|
||||
err = initActionFile(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create action file")
|
||||
}
|
||||
err = migrate.InitSchemaFile(ctx, fresh)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create migration file")
|
||||
if err := initConfigFile(ctx); err != nil {
|
||||
return errors.Wrap(err, "generating the config file")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -149,12 +105,12 @@ func initConfigFile(ctx infra.DnoteCtx) error {
|
|||
|
||||
b, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal config into YAML")
|
||||
return errors.Wrap(err, "marshalling config into YAML")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(path, b, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to write the config file at '%s'", path)
|
||||
return errors.Wrap(err, "writing the config file")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -175,353 +131,59 @@ func initDnoteDir(ctx infra.DnoteCtx) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// initDnoteFile creates an empty dnote file
|
||||
func initDnoteFile(ctx infra.DnoteCtx) error {
|
||||
path := GetDnotePath(ctx)
|
||||
|
||||
if utils.FileExists(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := json.Marshal(&infra.Dnote{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get initial dnote content")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(path, b, 0644)
|
||||
return err
|
||||
}
|
||||
|
||||
// initTimestampFile creates an empty dnote upgrade file
|
||||
func initTimestampFile(ctx infra.DnoteCtx) error {
|
||||
path := GetTimestampPath(ctx)
|
||||
|
||||
if utils.FileExists(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
ts := infra.Timestamp{
|
||||
LastUpgrade: now,
|
||||
}
|
||||
|
||||
b, err := yaml.Marshal(&ts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get initial timestamp content")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(path, b, 0644)
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadTimestamp gets the content of the timestamp file
|
||||
func ReadTimestamp(ctx infra.DnoteCtx) (infra.Timestamp, error) {
|
||||
var ret infra.Timestamp
|
||||
|
||||
path := GetTimestampPath(ctx)
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(b, &ret)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "Failed to unmarshal timestamp content")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func WriteTimestamp(ctx infra.DnoteCtx, timestamp infra.Timestamp) error {
|
||||
d, err := yaml.Marshal(timestamp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal timestamp into YAML")
|
||||
}
|
||||
|
||||
path := GetTimestampPath(ctx)
|
||||
err = ioutil.WriteFile(path, d, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write timestamp to the file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadNoteContent reads the content of dnote
|
||||
func ReadNoteContent(ctx infra.DnoteCtx) ([]byte, error) {
|
||||
notePath := GetDnotePath(ctx)
|
||||
|
||||
b, err := ioutil.ReadFile(notePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// GetDnote reads and parses the dnote
|
||||
func GetDnote(ctx infra.DnoteCtx) (infra.Dnote, error) {
|
||||
ret := infra.Dnote{}
|
||||
|
||||
b, err := ReadNoteContent(ctx)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "Failed to read note content")
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &ret)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "Failed to unmarshal note content")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// WriteDnote persists the state of Dnote into the dnote file
|
||||
func WriteDnote(ctx infra.DnoteCtx, dnote infra.Dnote) error {
|
||||
d, err := json.MarshalIndent(dnote, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notePath := GetDnotePath(ctx)
|
||||
|
||||
err = ioutil.WriteFile(notePath, d, 0644)
|
||||
if err != nil {
|
||||
errors.Wrap(err, "Failed to write to the dnote file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteConfig writes the config to the config file
|
||||
func WriteConfig(ctx infra.DnoteCtx, config infra.Config) error {
|
||||
d, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "marhsalling config")
|
||||
}
|
||||
|
||||
configPath := GetConfigPath(ctx)
|
||||
|
||||
err = ioutil.WriteFile(configPath, d, 0644)
|
||||
if err != nil {
|
||||
errors.Wrap(err, "Failed to write to the config file")
|
||||
errors.Wrap(err, "writing the config file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogAction appends the action to the action log and updates the last_action
|
||||
// timestamp
|
||||
func LogAction(ctx infra.DnoteCtx, action actions.Action) error {
|
||||
actions, err := ReadActionLog(ctx)
|
||||
// LogAction logs action and updates the last_action
|
||||
func LogAction(tx *sql.Tx, schema int, actionType, data string, timestamp int64) error {
|
||||
uuid := uuid.NewV4().String()
|
||||
|
||||
_, err := tx.Exec(`INSERT INTO actions (uuid, schema, type, data, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?)`, uuid, schema, actionType, data, timestamp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the action log")
|
||||
return errors.Wrap(err, "inserting an action")
|
||||
}
|
||||
|
||||
actions = append(actions, action)
|
||||
|
||||
err = WriteActionLog(ctx, actions)
|
||||
_, err = tx.Exec("UPDATE system SET value = ? WHERE key = ?", timestamp, "last_action")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write action log")
|
||||
}
|
||||
|
||||
err = UpdateLastActionTimestamp(ctx, action.Timestamp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to update the last_action timestamp")
|
||||
return errors.Wrap(err, "updating last_action")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteActionLog(ctx infra.DnoteCtx, ats []actions.Action) error {
|
||||
path := GetActionPath(ctx)
|
||||
|
||||
d, err := json.Marshal(ats)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal newly generated actions to JSON")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(path, d, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write to the actions file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClearActionLog(ctx infra.DnoteCtx) error {
|
||||
var content []actions.Action
|
||||
|
||||
if err := WriteActionLog(ctx, content); err != nil {
|
||||
return errors.Wrap(err, "Failed to write action log")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadActionLogContent(ctx infra.DnoteCtx) ([]byte, error) {
|
||||
path := GetActionPath(ctx)
|
||||
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return []byte{}, errors.Wrap(err, "Failed to read the action file")
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// ReadActionLog returns the action log content
|
||||
func ReadActionLog(ctx infra.DnoteCtx) ([]actions.Action, error) {
|
||||
var ret []actions.Action
|
||||
|
||||
b, err := ReadActionLogContent(ctx)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "Failed to read the action log content")
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &ret)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "Failed to unmarshal action log JSON")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// ReadConfig reads the config file
|
||||
func ReadConfig(ctx infra.DnoteCtx) (infra.Config, error) {
|
||||
var ret infra.Config
|
||||
|
||||
configPath := GetConfigPath(ctx)
|
||||
b, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
return ret, errors.Wrap(err, "reading config file")
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(b, &ret)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "Failed to unmarshal config YAML")
|
||||
return ret, errors.Wrap(err, "unmarshalling config")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func UpdateLastActionTimestamp(ctx infra.DnoteCtx, val int64) error {
|
||||
ts, err := ReadTimestamp(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the timestamp file")
|
||||
}
|
||||
|
||||
ts.LastAction = val
|
||||
|
||||
err = WriteTimestamp(ctx, ts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write the timestamp to the file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewNote returns a note
|
||||
func NewNote(content string, ts int64) infra.Note {
|
||||
return infra.Note{
|
||||
UUID: utils.GenerateUID(),
|
||||
Content: content,
|
||||
AddedOn: ts,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBook returns a book
|
||||
func NewBook(name string) infra.Book {
|
||||
return infra.Book{
|
||||
Name: name,
|
||||
Notes: make([]infra.Note, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func GetUpdatedBook(book infra.Book, notes []infra.Note) infra.Book {
|
||||
b := NewBook(book.Name)
|
||||
|
||||
b.Notes = notes
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// MigrateToDnoteDir creates dnote directory if artifacts from the previous version
|
||||
// of dnote are present, and moves the artifacts to the directory.
|
||||
func MigrateToDnoteDir(ctx infra.DnoteCtx) error {
|
||||
homeDir := ctx.HomeDir
|
||||
|
||||
temporaryDirPath := fmt.Sprintf("%s/.dnote-tmp", homeDir)
|
||||
oldDnotePath := fmt.Sprintf("%s/.dnote", homeDir)
|
||||
oldDnotercPath := fmt.Sprintf("%s/.dnoterc", homeDir)
|
||||
oldDnoteUpgradePath := fmt.Sprintf("%s/.dnote-upgrade", homeDir)
|
||||
|
||||
// Check if a dnote file exists. Return early if it does not exist,
|
||||
// or exists but already a directory.
|
||||
fi, err := os.Stat(oldDnotePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "Failed to look up old dnote path")
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.Mkdir(temporaryDirPath, 0755); err != nil {
|
||||
return errors.Wrap(err, "Failed to make temporary .dnote directory")
|
||||
}
|
||||
|
||||
// In the beta release for v0.2, backup user's .dnote
|
||||
if err := utils.CopyFile(oldDnotePath, fmt.Sprintf("%s/dnote-bak-5cdde2e83", homeDir)); err != nil {
|
||||
return errors.Wrap(err, "Failed to back up the old .dnote file")
|
||||
}
|
||||
|
||||
if err := os.Rename(oldDnotePath, fmt.Sprintf("%s/dnote", temporaryDirPath)); err != nil {
|
||||
return errors.Wrap(err, "Failed to move .dnote file")
|
||||
}
|
||||
if err := os.Rename(oldDnotercPath, fmt.Sprintf("%s/dnoterc", temporaryDirPath)); err != nil {
|
||||
return errors.Wrap(err, "Failed to move .dnoterc file")
|
||||
}
|
||||
if err := os.Remove(oldDnoteUpgradePath); err != nil {
|
||||
return errors.Wrap(err, "Failed to delete the old upgrade file")
|
||||
}
|
||||
|
||||
// Now that all files are moved to the temporary dir, rename the dir to .dnote
|
||||
if err := os.Rename(temporaryDirPath, fmt.Sprintf("%s/.dnote", homeDir)); err != nil {
|
||||
return errors.Wrap(err, "Failed to rename temporary dir to .dnote")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isFreshInstall checks if the dnote files have been initialized
|
||||
func isFreshInstall(ctx infra.DnoteCtx) (bool, error) {
|
||||
path := ctx.DnoteDir
|
||||
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return true, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "Failed to get file info for dnote directory")
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func FilterNotes(notes []infra.Note, testFunc func(infra.Note) bool) []infra.Note {
|
||||
var ret []infra.Note
|
||||
|
||||
for _, note := range notes {
|
||||
if testFunc(note) {
|
||||
ret = append(ret, note)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// SanitizeContent sanitizes note content
|
||||
func SanitizeContent(s string) string {
|
||||
var ret string
|
||||
|
|
@ -536,10 +198,10 @@ func SanitizeContent(s string) string {
|
|||
return ret
|
||||
}
|
||||
|
||||
func getEditorCmd(ctx infra.DnoteCtx, fpath string) (*exec.Cmd, error) {
|
||||
func newEditorCmd(ctx infra.DnoteCtx, fpath string) (*exec.Cmd, error) {
|
||||
config, err := ReadConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to read the config")
|
||||
return nil, errors.Wrap(err, "reading config")
|
||||
}
|
||||
|
||||
args := strings.Fields(config.Editor)
|
||||
|
|
@ -554,17 +216,17 @@ func GetEditorInput(ctx infra.DnoteCtx, fpath string, content *string) error {
|
|||
if !utils.FileExists(fpath) {
|
||||
f, err := os.Create(fpath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create a temporary file for content")
|
||||
return errors.Wrap(err, "creating a temporary content file")
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to close the temporary file for content")
|
||||
return errors.Wrap(err, "closing the temporary content file")
|
||||
}
|
||||
}
|
||||
|
||||
cmd, err := getEditorCmd(ctx, fpath)
|
||||
cmd, err := newEditorCmd(ctx, fpath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create the editor command")
|
||||
return errors.Wrap(err, "creating an editor command")
|
||||
}
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
|
|
@ -573,22 +235,22 @@ func GetEditorInput(ctx infra.DnoteCtx, fpath string, content *string) error {
|
|||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to launch the editor")
|
||||
return errors.Wrapf(err, "launching an editor")
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to wait for the editor")
|
||||
return errors.Wrap(err, "waiting for the editor")
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(fpath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the file")
|
||||
return errors.Wrap(err, "reading the temporary content file")
|
||||
}
|
||||
|
||||
err = os.Remove(fpath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to remove the temporary content file")
|
||||
return errors.Wrap(err, "removing the temporary content file")
|
||||
}
|
||||
|
||||
raw := string(b)
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"github.com/dnote/cli/testutils"
|
||||
"github.com/dnote/cli/utils"
|
||||
"github.com/pkg/errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMigrateToDnoteDir(t *testing.T) {
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
t.Run("pre v1 files exist", func(t *testing.T) {
|
||||
// set up
|
||||
if err := os.MkdirAll(ctx.HomeDir, 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(ctx.HomeDir); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
dnotePath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote"))
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to get absolute .dnote path").Error())
|
||||
}
|
||||
dnotercPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnoterc"))
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to get absolute .dnote path").Error())
|
||||
}
|
||||
dnoteUpgradePath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-upgrade"))
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to get absolute .dnote path").Error())
|
||||
}
|
||||
|
||||
if err = ioutil.WriteFile(dnotePath, []byte{}, 0644); err != nil {
|
||||
panic(errors.Wrap(err, "Failed prepare .dnote").Error())
|
||||
}
|
||||
if err = ioutil.WriteFile(dnotercPath, []byte{}, 0644); err != nil {
|
||||
panic(errors.Wrap(err, "Failed prepare .dnoterc").Error())
|
||||
}
|
||||
if err = ioutil.WriteFile(dnoteUpgradePath, []byte{}, 0644); err != nil {
|
||||
panic(errors.Wrap(err, "Failed prepare .dnote-upgrade").Error())
|
||||
}
|
||||
|
||||
// execute
|
||||
err = MigrateToDnoteDir(ctx)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to perform").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
newDnotePath, err := filepath.Abs(filepath.Join(ctx.DnoteDir, "dnote"))
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed get new dnote path").Error())
|
||||
}
|
||||
newDnotercPath, err := filepath.Abs(filepath.Join(ctx.DnoteDir, "dnoterc"))
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed get new dnoterc path").Error())
|
||||
}
|
||||
|
||||
fi, err := os.Stat(dnotePath)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to look up file"))
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
t.Fatal(".dnote must be a directory")
|
||||
}
|
||||
|
||||
if utils.FileExists(dnotercPath) {
|
||||
t.Error(".dnoterc must not exist in the original location")
|
||||
}
|
||||
if utils.FileExists(dnoteUpgradePath) {
|
||||
t.Error(".dnote-upgrade must not exist in the original location")
|
||||
}
|
||||
if !utils.FileExists(newDnotePath) {
|
||||
t.Error("dnote must exist")
|
||||
}
|
||||
if !utils.FileExists(newDnotercPath) {
|
||||
t.Error("dnoterc must exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
319
core/reducer.go
319
core/reducer.go
|
|
@ -1,20 +1,22 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"fmt"
|
||||
|
||||
"github.com/dnote/actions"
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/log"
|
||||
"github.com/dnote/cli/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ReduceAll reduces all actions
|
||||
func ReduceAll(ctx infra.DnoteCtx, ats []actions.Action) error {
|
||||
for _, action := range ats {
|
||||
if err := Reduce(ctx, action); err != nil {
|
||||
return errors.Wrap(err, "Failed to reduce action")
|
||||
func ReduceAll(ctx infra.DnoteCtx, tx *sql.Tx, actionSlice []actions.Action) error {
|
||||
for _, action := range actionSlice {
|
||||
if err := Reduce(ctx, tx, action); err != nil {
|
||||
return errors.Wrap(err, "reducing action")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -23,312 +25,205 @@ func ReduceAll(ctx infra.DnoteCtx, ats []actions.Action) error {
|
|||
|
||||
// Reduce transitions the local dnote state by consuming the action returned
|
||||
// from the server
|
||||
func Reduce(ctx infra.DnoteCtx, action actions.Action) error {
|
||||
func Reduce(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
|
||||
var err error
|
||||
|
||||
switch action.Type {
|
||||
case actions.ActionAddNote:
|
||||
err = handleAddNote(ctx, action)
|
||||
err = handleAddNote(ctx, tx, action)
|
||||
case actions.ActionRemoveNote:
|
||||
err = handleRemoveNote(ctx, action)
|
||||
err = handleRemoveNote(ctx, tx, action)
|
||||
case actions.ActionEditNote:
|
||||
err = handleEditNote(ctx, action)
|
||||
err = handleEditNote(ctx, tx, action)
|
||||
case actions.ActionAddBook:
|
||||
err = handleAddBook(ctx, action)
|
||||
err = handleAddBook(ctx, tx, action)
|
||||
case actions.ActionRemoveBook:
|
||||
err = handleRemoveBook(ctx, action)
|
||||
err = handleRemoveBook(ctx, tx, action)
|
||||
default:
|
||||
return errors.Errorf("Unsupported action %s", action.Type)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to process the action %s", action.Type)
|
||||
return errors.Wrapf(err, "reducing %s", action.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleAddNote(ctx infra.DnoteCtx, action actions.Action) error {
|
||||
func getBookUUIDWithTx(tx *sql.Tx, bookLabel string) (string, error) {
|
||||
var ret string
|
||||
err := tx.QueryRow("SELECT uuid FROM books WHERE label = ?", bookLabel).Scan(&ret)
|
||||
if err == sql.ErrNoRows {
|
||||
return ret, errors.Errorf("book '%s' not found", bookLabel)
|
||||
} else if err != nil {
|
||||
return ret, errors.Wrap(err, "querying the book")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func handleAddNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
|
||||
var data actions.AddNoteDataV1
|
||||
err := json.Unmarshal(action.Data, &data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to parse the action data")
|
||||
if err := json.Unmarshal(action.Data, &data); err != nil {
|
||||
return errors.Wrap(err, "parsing the action data")
|
||||
}
|
||||
|
||||
log.Debug("reducing add_note. action: %+v. data: %+v\n", action, data)
|
||||
|
||||
note := infra.Note{
|
||||
UUID: data.NoteUUID,
|
||||
Content: data.Content,
|
||||
AddedOn: action.Timestamp,
|
||||
}
|
||||
|
||||
dnote, err := GetDnote(ctx)
|
||||
bookUUID, err := getBookUUIDWithTx(tx, data.BookName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get dnote")
|
||||
}
|
||||
book, ok := dnote[data.BookName]
|
||||
if !ok {
|
||||
return errors.Errorf("Book with a name %s is not found", data.BookName)
|
||||
return errors.Wrap(err, "getting book uuid")
|
||||
}
|
||||
|
||||
// Check duplicate
|
||||
for _, note := range book.Notes {
|
||||
if note.UUID == data.NoteUUID {
|
||||
return errors.New("Duplicate note exists")
|
||||
}
|
||||
}
|
||||
|
||||
notes := append(dnote[book.Name].Notes, note)
|
||||
|
||||
sort.SliceStable(notes, func(i, j int) bool {
|
||||
return notes[i].AddedOn < notes[j].AddedOn
|
||||
})
|
||||
|
||||
dnote[book.Name] = GetUpdatedBook(dnote[book.Name], notes)
|
||||
|
||||
err = WriteDnote(ctx, dnote)
|
||||
var noteCount int
|
||||
err = tx.QueryRow("SELECT count(uuid) FROM notes WHERE uuid = ? AND book_uuid = ?", data.NoteUUID, bookUUID).Scan(¬eCount)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write dnote")
|
||||
return errors.Wrap(err, "counting note")
|
||||
}
|
||||
|
||||
if noteCount > 0 {
|
||||
// if a duplicate exists, it is because the same action has been previously synced to the server
|
||||
// but the client did not bring the bookmark up-to-date at the time because it had error reducing
|
||||
// the returned actions.
|
||||
// noop so that the client can update bookmark
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`INSERT INTO notes
|
||||
(uuid, book_uuid, content, added_on, public)
|
||||
VALUES (?, ?, ?, ?, ?)`, data.NoteUUID, bookUUID, data.Content, action.Timestamp, false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "inserting a note")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleRemoveNote(ctx infra.DnoteCtx, action actions.Action) error {
|
||||
func handleRemoveNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
|
||||
var data actions.RemoveNoteDataV1
|
||||
err := json.Unmarshal(action.Data, &data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to parse the action data")
|
||||
if err := json.Unmarshal(action.Data, &data); err != nil {
|
||||
return errors.Wrap(err, "parsing the action data")
|
||||
}
|
||||
|
||||
log.Debug("reducing remove_note. action: %+v. data: %+v\n", action, data)
|
||||
|
||||
dnote, err := GetDnote(ctx)
|
||||
_, err := tx.Exec("DELETE FROM notes WHERE uuid = ?", data.NoteUUID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get dnote")
|
||||
}
|
||||
book, ok := dnote[data.BookName]
|
||||
if !ok {
|
||||
return errors.Errorf("Book with a name %s is not found", data.BookName)
|
||||
}
|
||||
|
||||
notes := FilterNotes(book.Notes, func(note infra.Note) bool {
|
||||
return note.UUID != data.NoteUUID
|
||||
})
|
||||
dnote[book.Name] = GetUpdatedBook(dnote[book.Name], notes)
|
||||
|
||||
err = WriteDnote(ctx, dnote)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write dnote")
|
||||
return errors.Wrap(err, "removing a note")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleEditNoteV1(ctx infra.DnoteCtx, action actions.Action) error {
|
||||
var data actions.EditNoteDataV1
|
||||
err := json.Unmarshal(action.Data, &data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to parse the action data")
|
||||
func buildEditNoteQuery(ctx infra.DnoteCtx, tx *sql.Tx, noteUUID, bookUUID string, ts int64, data actions.EditNoteDataV2) (string, []interface{}, error) {
|
||||
setTmpl := "edited_on = ?"
|
||||
queryArgs := []interface{}{ts}
|
||||
|
||||
if data.Content != nil {
|
||||
setTmpl = fmt.Sprintf("%s, content = ?", setTmpl)
|
||||
queryArgs = append(queryArgs, *data.Content)
|
||||
}
|
||||
|
||||
log.Debug("reducing edit_note v1. action: %+v. data: %+v\n", action, data)
|
||||
|
||||
dnote, err := GetDnote(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get dnote")
|
||||
if data.Public != nil {
|
||||
setTmpl = fmt.Sprintf("%s, public = ?", setTmpl)
|
||||
queryArgs = append(queryArgs, *data.Public)
|
||||
}
|
||||
fromBook, ok := dnote[data.FromBook]
|
||||
if !ok {
|
||||
return errors.Errorf("Origin book with a name %s is not found", data.FromBook)
|
||||
}
|
||||
|
||||
if data.ToBook == "" {
|
||||
for idx, note := range fromBook.Notes {
|
||||
if note.UUID == data.NoteUUID {
|
||||
note.Content = data.Content
|
||||
note.EditedOn = action.Timestamp
|
||||
dnote[fromBook.Name].Notes[idx] = note
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Change the book
|
||||
|
||||
toBook, ok := dnote[data.ToBook]
|
||||
if !ok {
|
||||
return errors.Errorf("Destination book with a name %s is not found", data.FromBook)
|
||||
if data.ToBook != nil {
|
||||
bookUUID, err := getBookUUIDWithTx(tx, *data.ToBook)
|
||||
if err != nil {
|
||||
return "", []interface{}{}, errors.Wrap(err, "getting destination book uuid")
|
||||
}
|
||||
|
||||
var index int
|
||||
var note infra.Note
|
||||
|
||||
// Find the note
|
||||
for idx := range fromBook.Notes {
|
||||
note = fromBook.Notes[idx]
|
||||
|
||||
if note.UUID == data.NoteUUID {
|
||||
index = idx
|
||||
}
|
||||
}
|
||||
|
||||
note.Content = data.Content
|
||||
note.EditedOn = action.Timestamp
|
||||
|
||||
dnote[fromBook.Name] = GetUpdatedBook(dnote[fromBook.Name], append(fromBook.Notes[:index], fromBook.Notes[index+1:]...))
|
||||
dnote[toBook.Name] = GetUpdatedBook(dnote[toBook.Name], append(toBook.Notes, note))
|
||||
setTmpl = fmt.Sprintf("%s, book_uuid = ?", setTmpl)
|
||||
queryArgs = append(queryArgs, bookUUID)
|
||||
}
|
||||
|
||||
err = WriteDnote(ctx, dnote)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write dnote")
|
||||
}
|
||||
queryTmpl := fmt.Sprintf("UPDATE notes SET %s WHERE uuid = ? AND book_uuid = ?", setTmpl)
|
||||
queryArgs = append(queryArgs, noteUUID, bookUUID)
|
||||
|
||||
return nil
|
||||
return queryTmpl, queryArgs, nil
|
||||
}
|
||||
|
||||
func handleEditNoteV2(ctx infra.DnoteCtx, action actions.Action) error {
|
||||
func handleEditNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
|
||||
var data actions.EditNoteDataV2
|
||||
err := json.Unmarshal(action.Data, &data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to parse the action data")
|
||||
return errors.Wrap(err, "parsing the action data")
|
||||
}
|
||||
|
||||
log.Debug("reducing edit_note v2. action: %+v. data: %+v\n", action, data)
|
||||
|
||||
dnote, err := GetDnote(ctx)
|
||||
bookUUID, err := getBookUUIDWithTx(tx, data.FromBook)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get dnote")
|
||||
return errors.Wrap(err, "getting book uuid")
|
||||
}
|
||||
|
||||
fromBook, ok := dnote[data.FromBook]
|
||||
if !ok {
|
||||
return errors.Errorf("Origin book with a name %s is not found", data.FromBook)
|
||||
}
|
||||
|
||||
if data.ToBook == nil {
|
||||
for idx, note := range fromBook.Notes {
|
||||
if note.UUID == data.NoteUUID {
|
||||
if data.Content != nil {
|
||||
note.Content = *data.Content
|
||||
}
|
||||
if data.Public != nil {
|
||||
note.Public = *data.Public
|
||||
}
|
||||
|
||||
note.EditedOn = action.Timestamp
|
||||
dnote[fromBook.Name].Notes[idx] = note
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Change the book
|
||||
toBook := *data.ToBook
|
||||
|
||||
dstBook, ok := dnote[toBook]
|
||||
if !ok {
|
||||
return errors.Errorf("Destination book with a name %s is not found", toBook)
|
||||
}
|
||||
|
||||
var index int
|
||||
var note infra.Note
|
||||
|
||||
// Find the note
|
||||
for idx := range fromBook.Notes {
|
||||
note = fromBook.Notes[idx]
|
||||
|
||||
if note.UUID == data.NoteUUID {
|
||||
index = idx
|
||||
}
|
||||
}
|
||||
|
||||
if data.Content != nil {
|
||||
note.Content = *data.Content
|
||||
}
|
||||
if data.Public != nil {
|
||||
note.Public = *data.Public
|
||||
}
|
||||
note.EditedOn = action.Timestamp
|
||||
|
||||
dnote[fromBook.Name] = GetUpdatedBook(dnote[fromBook.Name], append(fromBook.Notes[:index], fromBook.Notes[index+1:]...))
|
||||
dnote[toBook] = GetUpdatedBook(dnote[toBook], append(dstBook.Notes, note))
|
||||
}
|
||||
|
||||
err = WriteDnote(ctx, dnote)
|
||||
queryTmpl, queryArgs, err := buildEditNoteQuery(ctx, tx, data.NoteUUID, bookUUID, action.Timestamp, data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write dnote")
|
||||
return errors.Wrap(err, "building edit note query")
|
||||
}
|
||||
_, err = tx.Exec(queryTmpl, queryArgs...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "updating a note")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleEditNote(ctx infra.DnoteCtx, action actions.Action) error {
|
||||
if action.Schema == 1 {
|
||||
return handleEditNoteV1(ctx, action)
|
||||
} else if action.Schema == 2 {
|
||||
return handleEditNoteV2(ctx, action)
|
||||
}
|
||||
|
||||
return errors.Errorf("Unsupported schema version for editing note: %d", action.Schema)
|
||||
}
|
||||
|
||||
func handleAddBook(ctx infra.DnoteCtx, action actions.Action) error {
|
||||
func handleAddBook(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
|
||||
var data actions.AddBookDataV1
|
||||
err := json.Unmarshal(action.Data, &data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to parse the action data")
|
||||
return errors.Wrap(err, "parsing the action data")
|
||||
}
|
||||
|
||||
log.Debug("reducing add_book. action: %+v. data: %+v\n", action, data)
|
||||
|
||||
dnote, err := GetDnote(ctx)
|
||||
var bookCount int
|
||||
err = tx.QueryRow("SELECT count(uuid) FROM books WHERE label = ?", data.BookName).Scan(&bookCount)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get dnote")
|
||||
return errors.Wrap(err, "counting books")
|
||||
}
|
||||
|
||||
_, exists := dnote[data.BookName]
|
||||
if exists {
|
||||
if bookCount > 0 {
|
||||
// If book already exists, another machine added a book with the same name.
|
||||
// noop
|
||||
return nil
|
||||
}
|
||||
|
||||
book := infra.Book{
|
||||
Name: data.BookName,
|
||||
Notes: []infra.Note{},
|
||||
}
|
||||
dnote[data.BookName] = book
|
||||
|
||||
err = WriteDnote(ctx, dnote)
|
||||
_, err = tx.Exec("INSERT INTO books (uuid, label) VALUES (?, ?)", utils.GenerateUUID(), data.BookName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write dnote")
|
||||
return errors.Wrap(err, "inserting a book")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleRemoveBook(ctx infra.DnoteCtx, action actions.Action) error {
|
||||
func handleRemoveBook(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
|
||||
var data actions.RemoveBookDataV1
|
||||
err := json.Unmarshal(action.Data, &data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to parse the action data")
|
||||
if err := json.Unmarshal(action.Data, &data); err != nil {
|
||||
return errors.Wrap(err, "parsing the action data")
|
||||
}
|
||||
|
||||
log.Debug("reducing remove_book. action: %+v. data: %+v\n", action, data)
|
||||
|
||||
dnote, err := GetDnote(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get dnote")
|
||||
var bookUUID string
|
||||
err := tx.QueryRow("SELECT uuid FROM books WHERE label = ?", data.BookName).Scan(&bookUUID)
|
||||
if err == sql.ErrNoRows {
|
||||
// If book does not exist, another client added and removed the book, making the add_book action
|
||||
// obsolete. noop.
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "querying the book")
|
||||
}
|
||||
|
||||
for bookName := range dnote {
|
||||
if bookName == data.BookName {
|
||||
delete(dnote, bookName)
|
||||
}
|
||||
_, err = tx.Exec("DELETE FROM notes WHERE book_uuid = ?", bookUUID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "removing notes")
|
||||
}
|
||||
|
||||
err = WriteDnote(ctx, dnote)
|
||||
_, err = tx.Exec("DELETE FROM books WHERE uuid = ?", bookUUID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write dnote")
|
||||
return errors.Wrap(err, "removing a book")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/dnote/actions"
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/testutils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestReduceAddNote(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
testutils.WriteFile(ctx, "../testutils/fixtures/dnote4.json", "dnote")
|
||||
testutils.Setup1(t, ctx)
|
||||
|
||||
// Execute
|
||||
b, err := json.Marshal(&actions.AddNoteDataV1{
|
||||
|
|
@ -28,23 +28,31 @@ func TestReduceAddNote(t *testing.T) {
|
|||
Data: b,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
if err = Reduce(ctx, action); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
|
||||
db := ctx.DB
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
if err = Reduce(ctx, tx, action); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "processing action"))
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
dnote, err := GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
var noteCount, jsNoteCount, linuxNoteCount int
|
||||
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
|
||||
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
|
||||
testutils.AssertEqual(t, noteCount, 2, "notes length mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, 2, "js notes length mismatch")
|
||||
testutils.AssertEqual(t, linuxNoteCount, 0, "linux notes length mismatch")
|
||||
|
||||
book := dnote["js"]
|
||||
otherBook := dnote["linux"]
|
||||
existingNote := book.Notes[0]
|
||||
newNote := book.Notes[1]
|
||||
var existingNote, newNote infra.Note
|
||||
testutils.MustScan(t, "scanning existing note", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &existingNote.UUID, &existingNote.Content)
|
||||
testutils.MustScan(t, "scanning new note", db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE uuid = ?", "06896551-8a06-4996-89cc-0d866308b0f6"), &newNote.UUID, &newNote.Content, &newNote.AddedOn)
|
||||
|
||||
testutils.AssertEqual(t, len(book.Notes), 2, "notes length mismatch")
|
||||
testutils.AssertEqual(t, len(otherBook.Notes), 0, "other book notes length mismatch")
|
||||
testutils.AssertEqual(t, existingNote.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "existing note uuid mismatch")
|
||||
testutils.AssertEqual(t, existingNote.Content, "Booleans have toString()", "existing note content mismatch")
|
||||
testutils.AssertEqual(t, newNote.UUID, "06896551-8a06-4996-89cc-0d866308b0f6", "new note uuid mismatch")
|
||||
|
|
@ -52,59 +60,12 @@ func TestReduceAddNote(t *testing.T) {
|
|||
testutils.AssertEqual(t, newNote.AddedOn, int64(1517629805), "new note added_on mismatch")
|
||||
}
|
||||
|
||||
func TestReduceAddNote_SortByAddedOn(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
|
||||
|
||||
// Execute
|
||||
b, err := json.Marshal(&actions.AddNoteDataV1{
|
||||
Content: "new content",
|
||||
BookName: "js",
|
||||
NoteUUID: "06896551-8a06-4996-89cc-0d866308b0f6",
|
||||
})
|
||||
action := actions.Action{
|
||||
Type: actions.ActionAddNote,
|
||||
Data: b,
|
||||
Timestamp: 1515199944,
|
||||
}
|
||||
if err = Reduce(ctx, action); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
}
|
||||
|
||||
// Test
|
||||
dnote, err := GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
|
||||
book := dnote["js"]
|
||||
otherBook := dnote["linux"]
|
||||
note1 := book.Notes[0]
|
||||
note2 := book.Notes[1]
|
||||
note3 := book.Notes[2]
|
||||
|
||||
testutils.AssertEqual(t, len(book.Notes), 3, "notes length mismatch")
|
||||
testutils.AssertEqual(t, len(otherBook.Notes), 1, "other book notes length mismatch")
|
||||
testutils.AssertEqual(t, note1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "existing note 1 uuid mismatch")
|
||||
testutils.AssertEqual(t, note1.Content, "Booleans have toString()", "existing note 1 content mismatch")
|
||||
testutils.AssertEqual(t, note2.UUID, "06896551-8a06-4996-89cc-0d866308b0f6", "new note uuid mismatch")
|
||||
testutils.AssertEqual(t, note2.Content, "new content", "new note content mismatch")
|
||||
testutils.AssertEqual(t, note2.AddedOn, int64(1515199944), "new note added_on mismatch")
|
||||
testutils.AssertEqual(t, note3.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "existing note 2 uuid mismatch")
|
||||
testutils.AssertEqual(t, note3.Content, "Date object implements mathematical comparisons", "existing note 2 content mismatch")
|
||||
}
|
||||
|
||||
func TestReduceRemoveNote(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
|
||||
testutils.Setup2(t, ctx)
|
||||
|
||||
// Execute
|
||||
b, err := json.Marshal(&actions.RemoveNoteDataV1{
|
||||
|
|
@ -116,268 +77,149 @@ func TestReduceRemoveNote(t *testing.T) {
|
|||
Data: b,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
if err = Reduce(ctx, action); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
|
||||
db := ctx.DB
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
if err = Reduce(ctx, tx, action); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "processing action"))
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
dnote, err := GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
var bookCount, noteCount, jsNoteCount, linuxNoteCount int
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
|
||||
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
|
||||
|
||||
targetBook := dnote["js"]
|
||||
otherBook := dnote["linux"]
|
||||
var n1, n2 infra.Note
|
||||
testutils.MustScan(t, "scanning note 1", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content)
|
||||
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "3e065d55-6d47-42f2-a6bf-f5844130b2d2"), &n2.UUID, &n2.Content)
|
||||
|
||||
testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch")
|
||||
testutils.AssertEqual(t, len(targetBook.Notes), 1, "target book notes length mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
|
||||
testutils.AssertEqual(t, len(otherBook.Notes), 1, "other book notes length mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch")
|
||||
testutils.AssertEqual(t, bookCount, 2, "number of books mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, 1, "target book notes length mismatch")
|
||||
testutils.AssertEqual(t, linuxNoteCount, 1, "other book notes length mismatch")
|
||||
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "remaining note content mismatch")
|
||||
testutils.AssertEqual(t, n2.UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, n2.Content, "wc -l to count words", "other book remaining note content mismatch")
|
||||
}
|
||||
|
||||
func TestReduceEditNote_V1_Content(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
func TestReduceEditNote(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data string
|
||||
expectedNoteUUID string
|
||||
expectedNoteBookUUID string
|
||||
expectedNoteContent string
|
||||
expectedNoteAddedOn int64
|
||||
expectedNoteEditedOn int64
|
||||
expectedNotePublic bool
|
||||
expectedJsNoteCount int
|
||||
expectedLinuxNoteCount int
|
||||
}{
|
||||
{
|
||||
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "content": "updated content"}`,
|
||||
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
|
||||
expectedNoteBookUUID: "js-book-uuid",
|
||||
expectedNoteContent: "updated content",
|
||||
expectedNoteAddedOn: int64(1515199951),
|
||||
expectedNoteEditedOn: int64(1517629805),
|
||||
expectedNotePublic: false,
|
||||
expectedJsNoteCount: 2,
|
||||
expectedLinuxNoteCount: 1,
|
||||
},
|
||||
{
|
||||
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "public": true}`,
|
||||
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
|
||||
expectedNoteBookUUID: "js-book-uuid",
|
||||
expectedNoteContent: "Date object implements mathematical comparisons",
|
||||
expectedNoteAddedOn: int64(1515199951),
|
||||
expectedNoteEditedOn: int64(1517629805),
|
||||
expectedNotePublic: true,
|
||||
expectedJsNoteCount: 2,
|
||||
expectedLinuxNoteCount: 1,
|
||||
},
|
||||
{
|
||||
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "to_book": "linux", "content": "updated content"}`,
|
||||
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
|
||||
expectedNoteBookUUID: "linux-book-uuid",
|
||||
expectedNoteContent: "updated content",
|
||||
expectedNoteAddedOn: int64(1515199951),
|
||||
expectedNoteEditedOn: int64(1517629805),
|
||||
expectedNotePublic: false,
|
||||
expectedJsNoteCount: 1,
|
||||
expectedLinuxNoteCount: 2,
|
||||
}}
|
||||
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
|
||||
for _, tc := range testCases {
|
||||
// Setup
|
||||
func() {
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// Execute
|
||||
b, err := json.Marshal(&actions.EditNoteDataV1{
|
||||
FromBook: "js",
|
||||
NoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
|
||||
Content: "updated content",
|
||||
})
|
||||
action := actions.Action{
|
||||
Type: actions.ActionEditNote,
|
||||
Data: b,
|
||||
Schema: 1,
|
||||
Timestamp: 1517629805,
|
||||
testutils.Setup2(t, ctx)
|
||||
db := ctx.DB
|
||||
|
||||
// Execute
|
||||
action := actions.Action{
|
||||
Type: actions.ActionEditNote,
|
||||
Data: json.RawMessage(tc.data),
|
||||
Schema: 2,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
err = Reduce(ctx, tx, action)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
var bookCount, noteCount, jsNoteCount, linuxNoteCount int
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
|
||||
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
|
||||
|
||||
var n1, n2, n3 infra.Note
|
||||
testutils.MustScan(t, "scanning note 1", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content)
|
||||
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "3e065d55-6d47-42f2-a6bf-f5844130b2d2"), &n2.UUID, &n2.Content)
|
||||
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content, added_on, edited_on, public FROM notes WHERE uuid = ?", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n3.UUID, &n3.Content, &n3.AddedOn, &n3.EditedOn, &n3.Public)
|
||||
|
||||
testutils.AssertEqual(t, bookCount, 2, "number of books mismatch")
|
||||
testutils.AssertEqual(t, noteCount, 3, "number of notes mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, tc.expectedJsNoteCount, "js book notes length mismatch")
|
||||
testutils.AssertEqual(t, linuxNoteCount, tc.expectedLinuxNoteCount, "linux book notes length mismatch")
|
||||
|
||||
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "n1 mismatch")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "n1 content mismatch")
|
||||
testutils.AssertEqual(t, n2.UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "n2 uuid mismatch")
|
||||
testutils.AssertEqual(t, n2.Content, "wc -l to count words", "n2 content mismatch")
|
||||
testutils.AssertEqual(t, n3.UUID, tc.expectedNoteUUID, "edited note uuid mismatch")
|
||||
testutils.AssertEqual(t, n3.Content, tc.expectedNoteContent, "edited note content mismatch")
|
||||
testutils.AssertEqual(t, n3.AddedOn, tc.expectedNoteAddedOn, "edited note added_on mismatch")
|
||||
testutils.AssertEqual(t, n3.EditedOn, tc.expectedNoteEditedOn, "edited note edited_on mismatch")
|
||||
testutils.AssertEqual(t, n3.Public, tc.expectedNotePublic, "edited note public mismatch")
|
||||
}()
|
||||
}
|
||||
err = Reduce(ctx, action)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
}
|
||||
|
||||
// Test
|
||||
dnote, err := GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
|
||||
targetBook := dnote["js"]
|
||||
otherBook := dnote["linux"]
|
||||
|
||||
testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch")
|
||||
testutils.AssertEqual(t, len(targetBook.Notes), 2, "target book notes length mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[1].Content, "updated content", "edited note content mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[1].EditedOn, int64(1517629805), "edited note edited_on mismatch")
|
||||
testutils.AssertEqual(t, len(otherBook.Notes), 1, "other book notes length mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch")
|
||||
}
|
||||
|
||||
func TestReduceEditNote_V1_ChangeBook(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
|
||||
|
||||
// Execute
|
||||
b, err := json.Marshal(&actions.EditNoteDataV1{
|
||||
FromBook: "js",
|
||||
ToBook: "linux",
|
||||
NoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
|
||||
Content: "updated content",
|
||||
})
|
||||
action := actions.Action{
|
||||
Type: actions.ActionEditNote,
|
||||
Data: b,
|
||||
Schema: 1,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
err = Reduce(ctx, action)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
}
|
||||
|
||||
// Test
|
||||
dnote, err := GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
|
||||
targetBook := dnote["js"]
|
||||
otherBook := dnote["linux"]
|
||||
|
||||
if len(targetBook.Notes) != 1 {
|
||||
t.Fatalf("target book length mismatch. Got %d", len(targetBook.Notes))
|
||||
}
|
||||
if len(otherBook.Notes) != 2 {
|
||||
t.Fatalf("other book length mismatch. Got %d", len(targetBook.Notes))
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[1].Content, "updated content", "edited note content mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[1].EditedOn, int64(1517629805), "edited note edited_on mismatch")
|
||||
}
|
||||
|
||||
func TestReduceEditNote_V2_Content(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
|
||||
|
||||
// Execute
|
||||
b := json.RawMessage(`{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "content": "updated content"}`)
|
||||
|
||||
action := actions.Action{
|
||||
Type: actions.ActionEditNote,
|
||||
Data: b,
|
||||
Schema: 2,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
err := Reduce(ctx, action)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
}
|
||||
|
||||
// Test
|
||||
dnote, err := GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
|
||||
targetBook := dnote["js"]
|
||||
otherBook := dnote["linux"]
|
||||
|
||||
testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch")
|
||||
testutils.AssertEqual(t, len(targetBook.Notes), 2, "target book notes length mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[1].Content, "updated content", "edited note content mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[1].EditedOn, int64(1517629805), "edited note edited_on mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[1].Public, false, "edited note public mismatch")
|
||||
testutils.AssertEqual(t, len(otherBook.Notes), 1, "other book notes length mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch")
|
||||
}
|
||||
|
||||
func TestReduceEditNote_V2_public(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
|
||||
|
||||
// Execute
|
||||
b := json.RawMessage(`{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "public": true}`)
|
||||
|
||||
action := actions.Action{
|
||||
Type: actions.ActionEditNote,
|
||||
Data: b,
|
||||
Schema: 2,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
err := Reduce(ctx, action)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
}
|
||||
|
||||
// Test
|
||||
dnote, err := GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
|
||||
targetBook := dnote["js"]
|
||||
otherBook := dnote["linux"]
|
||||
|
||||
testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch")
|
||||
testutils.AssertEqual(t, len(targetBook.Notes), 2, "target book notes length mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[1].Content, "Date object implements mathematical comparisons", "edited note content mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[1].EditedOn, int64(1517629805), "edited note edited_on mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[1].Public, true, "edited note public mismatch")
|
||||
testutils.AssertEqual(t, len(otherBook.Notes), 1, "other book notes length mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch")
|
||||
}
|
||||
|
||||
func TestReduceEditNote_V2_changeBook(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
|
||||
|
||||
// Execute
|
||||
b := json.RawMessage(`{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "from_book": "js", "to_book": "linux", "content": "updated content"}`)
|
||||
action := actions.Action{
|
||||
Type: actions.ActionEditNote,
|
||||
Data: b,
|
||||
Schema: 2,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
err := Reduce(ctx, action)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
}
|
||||
|
||||
// Test
|
||||
dnote, err := GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
|
||||
targetBook := dnote["js"]
|
||||
otherBook := dnote["linux"]
|
||||
|
||||
if len(targetBook.Notes) != 1 {
|
||||
t.Fatalf("target book length mismatch. Got %d", len(targetBook.Notes))
|
||||
}
|
||||
if len(otherBook.Notes) != 2 {
|
||||
t.Fatalf("other book length mismatch. Got %d", len(targetBook.Notes))
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[1].Content, "updated content", "edited note content mismatch")
|
||||
testutils.AssertEqual(t, otherBook.Notes[1].EditedOn, int64(1517629805), "edited note edited_on mismatch")
|
||||
}
|
||||
|
||||
func TestReduceAddBook(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
testutils.WriteFile(ctx, "../testutils/fixtures/dnote4.json", "dnote")
|
||||
testutils.Setup1(t, ctx)
|
||||
|
||||
// Execute
|
||||
b, err := json.Marshal(&actions.AddBookDataV1{BookName: "new_book"})
|
||||
|
|
@ -386,30 +228,32 @@ func TestReduceAddBook(t *testing.T) {
|
|||
Data: b,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
if err = Reduce(ctx, action); err != nil {
|
||||
db := ctx.DB
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
if err = Reduce(ctx, tx, action); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
dnote, err := GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
var bookCount, newBookNoteCount int
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting note in the new book", db.QueryRow("SELECT count(*) FROM notes INNER JOIN books ON books.uuid = notes.book_uuid WHERE books.label = ?", "new_book"), &newBookNoteCount)
|
||||
|
||||
newBook := dnote["new_book"]
|
||||
|
||||
testutils.AssertEqual(t, len(dnote), 3, "number of books mismatch")
|
||||
testutils.AssertEqual(t, newBook.Name, "new_book", "new book name mismatch")
|
||||
testutils.AssertEqual(t, len(newBook.Notes), 0, "new book number of notes mismatch")
|
||||
testutils.AssertEqual(t, bookCount, 3, "number of books mismatch")
|
||||
testutils.AssertEqual(t, newBookNoteCount, 0, "new book number of notes mismatch")
|
||||
}
|
||||
|
||||
func TestReduceRemoveBook(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote")
|
||||
testutils.Setup2(t, ctx)
|
||||
|
||||
// Execute
|
||||
b, err := json.Marshal(&actions.RemoveBookDataV1{BookName: "linux"})
|
||||
|
|
@ -418,23 +262,39 @@ func TestReduceRemoveBook(t *testing.T) {
|
|||
Data: b,
|
||||
Timestamp: 1517629805,
|
||||
}
|
||||
if err = Reduce(ctx, action); err != nil {
|
||||
|
||||
db := ctx.DB
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
if err = Reduce(ctx, tx, action); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "Failed to process action"))
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
dnote, err := GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
var bookCount, noteCount, jsNoteCount, linuxNoteCount int
|
||||
var jsBookLabel string
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
|
||||
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
|
||||
testutils.MustScan(t, "scanning book", db.QueryRow("SELECT label FROM books WHERE uuid = ?", "js-book-uuid"), &jsBookLabel)
|
||||
|
||||
remainingBook := dnote["js"]
|
||||
var n1, n2 infra.Note
|
||||
testutils.MustScan(t, "scanning note 1", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content)
|
||||
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n2.UUID, &n2.Content)
|
||||
|
||||
testutils.AssertEqual(t, len(dnote), 1, "number of books mismatch")
|
||||
testutils.AssertEqual(t, remainingBook.Name, "js", "remaining book name mismatch")
|
||||
testutils.AssertEqual(t, len(remainingBook.Notes), 2, "remaining book number of notes mismatch")
|
||||
testutils.AssertEqual(t, remainingBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, remainingBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch")
|
||||
testutils.AssertEqual(t, remainingBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
|
||||
testutils.AssertEqual(t, remainingBook.Notes[1].Content, "Date object implements mathematical comparisons", "edited note content mismatch")
|
||||
testutils.AssertEqual(t, bookCount, 1, "number of books mismatch")
|
||||
testutils.AssertEqual(t, noteCount, 2, "number of notes mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, 2, "js note count mismatch")
|
||||
testutils.AssertEqual(t, linuxNoteCount, 0, "linux note count mismatch")
|
||||
testutils.AssertEqual(t, jsBookLabel, "js", "remaining book name mismatch")
|
||||
|
||||
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "remaining note content mismatch")
|
||||
testutils.AssertEqual(t, n2.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
|
||||
testutils.AssertEqual(t, n2.Content, "Date object implements mathematical comparisons", "edited note content mismatch")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,27 +17,26 @@ var upgradeInterval int64 = 86400 * 7 * 3
|
|||
|
||||
// shouldCheckUpdate checks if update should be checked
|
||||
func shouldCheckUpdate(ctx infra.DnoteCtx) (bool, error) {
|
||||
timestamp, err := ReadTimestamp(ctx)
|
||||
db := ctx.DB
|
||||
|
||||
var lastUpgrade int64
|
||||
err := db.QueryRow("SELECT value FROM system WHERE key = ?", "last_upgrade").Scan(&lastUpgrade)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "Failed to get timestamp content")
|
||||
return false, errors.Wrap(err, "getting last_udpate")
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
return now-timestamp.LastUpgrade > upgradeInterval, nil
|
||||
return now-lastUpgrade > upgradeInterval, nil
|
||||
}
|
||||
|
||||
func touchLastUpgrade(ctx infra.DnoteCtx) error {
|
||||
timestamp, err := ReadTimestamp(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get timestamp content")
|
||||
}
|
||||
db := ctx.DB
|
||||
|
||||
now := time.Now().Unix()
|
||||
timestamp.LastUpgrade = now
|
||||
|
||||
if err := WriteTimestamp(ctx, timestamp); err != nil {
|
||||
return errors.Wrap(err, "Failed to write the updated timestamp to the file")
|
||||
_, err := db.Exec("UPDATE system SET value = ? WHERE key = ?", now, "last_upgrade")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "updating last_upgrade")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
174
infra/main.go
174
infra/main.go
|
|
@ -1,12 +1,31 @@
|
|||
// Package infra defines dnote structure
|
||||
package infra
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"time"
|
||||
|
||||
// use sqlite
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// DnoteDirName is the name of the directory containing dnote files
|
||||
DnoteDirName = ".dnote"
|
||||
)
|
||||
|
||||
// DnoteCtx is a context holding the information of the current runtime
|
||||
type DnoteCtx struct {
|
||||
HomeDir string
|
||||
DnoteDir string
|
||||
APIEndpoint string
|
||||
Version string
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
// Config holds dnote configuration
|
||||
|
|
@ -41,3 +60,158 @@ type Timestamp struct {
|
|||
// timestamp of the most recent action performed by the cli
|
||||
LastAction int64 `yaml:"last_action"`
|
||||
}
|
||||
|
||||
// NewCtx returns a new dnote context
|
||||
func NewCtx(apiEndpoint, versionTag string) (DnoteCtx, error) {
|
||||
homeDir, err := getHomeDir()
|
||||
if err != nil {
|
||||
return DnoteCtx{}, errors.Wrap(err, "Failed to get home dir")
|
||||
}
|
||||
dnoteDir := getDnoteDir(homeDir)
|
||||
|
||||
dnoteDBPath := fmt.Sprintf("%s/dnote.db", dnoteDir)
|
||||
db, err := sql.Open("sqlite3", dnoteDBPath)
|
||||
if err != nil {
|
||||
return DnoteCtx{}, errors.Wrap(err, "conntecting to db")
|
||||
}
|
||||
|
||||
ret := DnoteCtx{
|
||||
HomeDir: homeDir,
|
||||
DnoteDir: dnoteDir,
|
||||
APIEndpoint: apiEndpoint,
|
||||
Version: versionTag,
|
||||
DB: db,
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func getDnoteDir(homeDir string) string {
|
||||
var ret string
|
||||
|
||||
dnoteDirEnv := os.Getenv("DNOTE_DIR")
|
||||
if dnoteDirEnv == "" {
|
||||
ret = fmt.Sprintf("%s/%s", homeDir, DnoteDirName)
|
||||
} else {
|
||||
ret = dnoteDirEnv
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func getHomeDir() (string, error) {
|
||||
homeDirEnv := os.Getenv("DNOTE_HOME_DIR")
|
||||
if homeDirEnv != "" {
|
||||
return homeDirEnv, nil
|
||||
}
|
||||
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Failed to get current user")
|
||||
}
|
||||
|
||||
return usr.HomeDir, nil
|
||||
}
|
||||
|
||||
// InitDB initializes the database.
|
||||
// Ideally this process must be a part of migration sequence. But it is performed
|
||||
// seaprately because it is a prerequisite for legacy migration.
|
||||
func InitDB(ctx DnoteCtx) error {
|
||||
db := ctx.DB
|
||||
|
||||
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS notes
|
||||
(
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
uuid text NOT NULL,
|
||||
book_uuid text NOT NULL,
|
||||
content text NOT NULL,
|
||||
added_on integer NOT NULL,
|
||||
edited_on integer DEFAULT 0,
|
||||
public bool DEFAULT false
|
||||
)`)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating notes table")
|
||||
}
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS books
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
label text NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating books table")
|
||||
}
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS actions
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
schema integer NOT NULL,
|
||||
type text NOT NULL,
|
||||
data text NOT NULL,
|
||||
timestamp integer NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating actions table")
|
||||
}
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS system
|
||||
(
|
||||
key string NOT NULL,
|
||||
value text NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating system table")
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_books_label ON books(label);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_uuid ON notes(uuid);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_books_uuid ON books(uuid);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_id ON notes(id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_book_uuid ON notes(book_uuid);`)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating indices")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitSystem inserts system data if missing
|
||||
func InitSystem(ctx DnoteCtx) error {
|
||||
db := ctx.DB
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
|
||||
var bookmarkCount, lastUpgradeCount int
|
||||
if err := db.QueryRow("SELECT count(*) FROM system WHERE key = ?", "bookmark").
|
||||
Scan(&bookmarkCount); err != nil {
|
||||
return errors.Wrap(err, "counting bookmarks")
|
||||
}
|
||||
if bookmarkCount == 0 {
|
||||
_, err := tx.Exec("INSERT INTO system (key, value) VALUES (?, ?)", "bookmark", 0)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "inserting bookmark")
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.QueryRow("SELECT count(*) FROM system WHERE key = ?", "last_upgrade").
|
||||
Scan(&lastUpgradeCount); err != nil {
|
||||
return errors.Wrap(err, "counting last_upgrade")
|
||||
}
|
||||
if lastUpgradeCount == 0 {
|
||||
now := time.Now().Unix()
|
||||
_, err := tx.Exec("INSERT INTO system (key, value) VALUES (?, ?)", "last_upgrade", now)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "inserting bookmark")
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
59
main.go
59
main.go
|
|
@ -1,14 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
|
||||
"github.com/dnote/cli/cmd/root"
|
||||
"github.com/dnote/cli/core"
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/log"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
// commands
|
||||
|
|
@ -17,6 +15,7 @@ import (
|
|||
"github.com/dnote/cli/cmd/edit"
|
||||
"github.com/dnote/cli/cmd/login"
|
||||
"github.com/dnote/cli/cmd/ls"
|
||||
|
||||
"github.com/dnote/cli/cmd/remove"
|
||||
"github.com/dnote/cli/cmd/sync"
|
||||
"github.com/dnote/cli/cmd/version"
|
||||
|
|
@ -28,14 +27,14 @@ var apiEndpoint string
|
|||
var versionTag = "master"
|
||||
|
||||
func main() {
|
||||
ctx, err := newCtx()
|
||||
ctx, err := infra.NewCtx(apiEndpoint, versionTag)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to initialize the dnote context"))
|
||||
panic(errors.Wrap(err, "initializing context"))
|
||||
}
|
||||
defer ctx.DB.Close()
|
||||
|
||||
err = root.Prepare(ctx)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to prepare dnote run"))
|
||||
if err := root.Prepare(ctx); err != nil {
|
||||
panic(errors.Wrap(err, "preparing dnote run"))
|
||||
}
|
||||
|
||||
root.Register(remove.NewCmd(ctx))
|
||||
|
|
@ -53,47 +52,3 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func newCtx() (infra.DnoteCtx, error) {
|
||||
homeDir, err := getHomeDir()
|
||||
if err != nil {
|
||||
return infra.DnoteCtx{}, errors.Wrap(err, "Failed to get home dir")
|
||||
}
|
||||
dnoteDir := getDnoteDir(homeDir)
|
||||
|
||||
ret := infra.DnoteCtx{
|
||||
HomeDir: homeDir,
|
||||
DnoteDir: dnoteDir,
|
||||
APIEndpoint: apiEndpoint,
|
||||
Version: versionTag,
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func getDnoteDir(homeDir string) string {
|
||||
var ret string
|
||||
|
||||
dnoteDirEnv := os.Getenv("DNOTE_DIR")
|
||||
if dnoteDirEnv == "" {
|
||||
ret = fmt.Sprintf("%s/%s", homeDir, core.DnoteDirName)
|
||||
} else {
|
||||
ret = dnoteDirEnv
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func getHomeDir() (string, error) {
|
||||
homeDirEnv := os.Getenv("DNOTE_HOME_DIR")
|
||||
if homeDirEnv != "" {
|
||||
return homeDirEnv, nil
|
||||
}
|
||||
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Failed to get current user")
|
||||
}
|
||||
|
||||
return usr.HomeDir, nil
|
||||
}
|
||||
|
|
|
|||
446
main_test.go
446
main_test.go
|
|
@ -1,14 +1,11 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -24,353 +21,298 @@ var binaryName = "test-dnote"
|
|||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := exec.Command("go", "build", "-o", binaryName).Run(); err != nil {
|
||||
log.Print(errors.Wrap(err, "Failed to build a binary").Error())
|
||||
log.Print(errors.Wrap(err, "building a binary").Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func newDnoteCmd(ctx infra.DnoteCtx, arg ...string) (*exec.Cmd, *bytes.Buffer, error) {
|
||||
var stderr bytes.Buffer
|
||||
|
||||
binaryPath, err := filepath.Abs(binaryName)
|
||||
if err != nil {
|
||||
return &exec.Cmd{}, &stderr, errors.Wrap(err, "Failed to get the absolute path to the test binary")
|
||||
}
|
||||
|
||||
cmd := exec.Command(binaryPath, arg...)
|
||||
cmd.Env = []string{fmt.Sprintf("DNOTE_DIR=%s", ctx.DnoteDir), fmt.Sprintf("DNOTE_HOME_DIR=%s", ctx.HomeDir)}
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
return cmd, &stderr, nil
|
||||
}
|
||||
|
||||
func runDnoteCmd(ctx infra.DnoteCtx, arg ...string) {
|
||||
cmd, stderr, err := newDnoteCmd(ctx, arg...)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to get command").Error())
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
panic(errors.Wrapf(err, "Failed to run command %s", stderr.String()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("./tmp")
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
// Set up
|
||||
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// Execute
|
||||
runDnoteCmd(ctx)
|
||||
testutils.RunDnoteCmd(t, ctx, binaryName)
|
||||
|
||||
// Test
|
||||
if !utils.FileExists(ctx.DnoteDir) {
|
||||
t.Errorf("dnote directory was not initialized")
|
||||
}
|
||||
if !utils.FileExists(fmt.Sprintf("%s/%s", ctx.DnoteDir, core.DnoteFilename)) {
|
||||
t.Errorf("dnote file was not initialized")
|
||||
}
|
||||
if !utils.FileExists(fmt.Sprintf("%s/%s", ctx.DnoteDir, core.ConfigFilename)) {
|
||||
t.Errorf("config file was not initialized")
|
||||
}
|
||||
if !utils.FileExists(fmt.Sprintf("%s/%s", ctx.DnoteDir, core.TimestampFilename)) {
|
||||
t.Errorf("timestamp file was not initialized")
|
||||
}
|
||||
if !utils.FileExists(fmt.Sprintf("%s/%s", ctx.DnoteDir, core.ActionFilename)) {
|
||||
t.Errorf("action file was not initialized")
|
||||
}
|
||||
|
||||
db := ctx.DB
|
||||
|
||||
var notesTableCount, booksTableCount, actionsTableCount, systemTableCount int
|
||||
testutils.MustScan(t, "counting notes",
|
||||
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "notes"), ¬esTableCount)
|
||||
testutils.MustScan(t, "counting books",
|
||||
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "books"), &booksTableCount)
|
||||
testutils.MustScan(t, "counting actions",
|
||||
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "actions"), &actionsTableCount)
|
||||
testutils.MustScan(t, "counting system",
|
||||
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "system"), &systemTableCount)
|
||||
|
||||
testutils.AssertEqual(t, notesTableCount, 1, "notes table count mismatch")
|
||||
testutils.AssertEqual(t, booksTableCount, 1, "books table count mismatch")
|
||||
testutils.AssertEqual(t, actionsTableCount, 1, "actions table count mismatch")
|
||||
testutils.AssertEqual(t, systemTableCount, 1, "system table count mismatch")
|
||||
}
|
||||
|
||||
func TestAdd_NewBook_ContentFlag(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("./tmp")
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
func TestAddNote_NewBook_ContentFlag(t *testing.T) {
|
||||
// Set up
|
||||
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// Execute
|
||||
runDnoteCmd(ctx, "add", "js", "-c", "foo")
|
||||
testutils.RunDnoteCmd(t, ctx, binaryName, "add", "js", "-c", "foo")
|
||||
|
||||
// Test
|
||||
dnote, err := core.GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
actionSlice, err := core.ReadActionLog(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to read actions"))
|
||||
}
|
||||
db := ctx.DB
|
||||
|
||||
if len(actionSlice) != 2 {
|
||||
t.Fatalf("action log length mismatch. got %d", len(actionSlice))
|
||||
}
|
||||
var actionCount, noteCount, bookCount int
|
||||
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
|
||||
book := dnote["js"]
|
||||
note := book.Notes[0]
|
||||
bookAction := actionSlice[0]
|
||||
noteAction := actionSlice[1]
|
||||
testutils.AssertEqualf(t, actionCount, 2, "action count mismatch")
|
||||
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
|
||||
testutils.AssertEqualf(t, noteCount, 1, "note count mismatch")
|
||||
|
||||
var jsBookUUID string
|
||||
testutils.MustScan(t, "getting js book uuid", db.QueryRow("SELECT uuid FROM books where label = ?", "js"), &jsBookUUID)
|
||||
var note infra.Note
|
||||
testutils.MustScan(t, "getting note",
|
||||
db.QueryRow("SELECT uuid, content, added_on FROM notes where book_uuid = ?", jsBookUUID), ¬e.UUID, ¬e.Content, ¬e.AddedOn)
|
||||
var bookAction, noteAction actions.Action
|
||||
testutils.MustScan(t, "getting book action",
|
||||
db.QueryRow("SELECT data, timestamp FROM actions where type = ?", actions.ActionAddBook), &bookAction.Data, &bookAction.Timestamp)
|
||||
testutils.MustScan(t, "getting note action",
|
||||
db.QueryRow("SELECT data, timestamp FROM actions where type = ?", actions.ActionAddNote), ¬eAction.Data, ¬eAction.Timestamp)
|
||||
|
||||
var noteActionData actions.AddNoteDataV1
|
||||
var bookActionData actions.AddBookDataV1
|
||||
err = json.Unmarshal(bookAction.Data, &bookActionData)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to unmarshal the action data: %s", err)
|
||||
if err := json.Unmarshal(bookAction.Data, &bookActionData); err != nil {
|
||||
log.Fatalf("unmarshalling the action data: %s", err)
|
||||
}
|
||||
err = json.Unmarshal(noteAction.Data, ¬eActionData)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to unmarshal the action data: %s", err)
|
||||
if err := json.Unmarshal(noteAction.Data, ¬eActionData); err != nil {
|
||||
log.Fatalf("unmarshalling the action data: %s", err)
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, bookAction.Type, actions.ActionAddBook, "bookAction type mismatch")
|
||||
testutils.AssertNotEqual(t, bookActionData.BookName, "", "bookAction data note_uuid mismatch")
|
||||
testutils.AssertNotEqual(t, bookAction.Timestamp, 0, "bookAction timestamp mismatch")
|
||||
testutils.AssertEqual(t, noteAction.Type, actions.ActionAddNote, "noteAction type mismatch")
|
||||
testutils.AssertEqual(t, noteActionData.Content, "foo", "noteAction data name mismatch")
|
||||
testutils.AssertNotEqual(t, noteActionData.NoteUUID, nil, "noteAction data note_uuid mismatch")
|
||||
testutils.AssertNotEqual(t, noteActionData.BookName, "", "noteAction data note_uuid mismatch")
|
||||
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "noteAction timestamp mismatch")
|
||||
testutils.AssertEqual(t, len(book.Notes), 1, "Book should have one note")
|
||||
testutils.AssertNotEqual(t, note.UUID, "", "Note should have UUID")
|
||||
testutils.AssertEqual(t, note.Content, "foo", "Note content mismatch")
|
||||
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "Note added_on mismatch")
|
||||
}
|
||||
|
||||
func TestAdd_ExistingBook_ContentFlag(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("./tmp")
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
func TestAddNote_ExistingBook_ContentFlag(t *testing.T) {
|
||||
// Set up
|
||||
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// init files by running root command
|
||||
runDnoteCmd(ctx)
|
||||
testutils.WriteFile(ctx, "./testutils/fixtures/dnote1.json", "dnote")
|
||||
testutils.Setup3(t, ctx)
|
||||
|
||||
// Execute
|
||||
runDnoteCmd(ctx, "add", "js", "-c", "foo")
|
||||
testutils.RunDnoteCmd(t, ctx, binaryName, "add", "js", "-c", "foo")
|
||||
|
||||
// Test
|
||||
dnote, err := core.GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
actionSlice, err := core.ReadActionLog(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to read actions"))
|
||||
db := ctx.DB
|
||||
|
||||
var actionCount, noteCount, bookCount int
|
||||
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
|
||||
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
|
||||
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
|
||||
testutils.AssertEqualf(t, noteCount, 2, "note count mismatch")
|
||||
|
||||
var n1, n2 infra.Note
|
||||
testutils.MustScan(t, "getting n1",
|
||||
db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE book_uuid = ? AND uuid = ?", "js-book-uuid", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content, &n1.AddedOn)
|
||||
testutils.MustScan(t, "getting n2",
|
||||
db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE book_uuid = ? AND content = ?", "js-book-uuid", "foo"), &n2.UUID, &n2.Content, &n2.AddedOn)
|
||||
var noteAction actions.Action
|
||||
testutils.MustScan(t, "getting note action",
|
||||
db.QueryRow("SELECT data, timestamp FROM actions WHERE type = ?", actions.ActionAddNote), ¬eAction.Data, ¬eAction.Timestamp)
|
||||
|
||||
var noteActionData actions.AddNoteDataV1
|
||||
if err := json.Unmarshal(noteAction.Data, ¬eActionData); err != nil {
|
||||
log.Fatalf("unmarshalling the action data: %s", err)
|
||||
}
|
||||
|
||||
book := dnote["js"]
|
||||
action := actionSlice[0]
|
||||
|
||||
var actionData actions.AddNoteDataV1
|
||||
err = json.Unmarshal(action.Data, &actionData)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to unmarshal the action data: %s", err)
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, len(actionSlice), 1, "There should be 1 action")
|
||||
testutils.AssertEqual(t, action.Type, actions.ActionAddNote, "action type mismatch")
|
||||
testutils.AssertEqual(t, actionData.Content, "foo", "action data name mismatch")
|
||||
testutils.AssertNotEqual(t, actionData.NoteUUID, "", "action data note_uuid mismatch")
|
||||
testutils.AssertEqual(t, actionData.BookName, "js", "action data book_name mismatch")
|
||||
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertEqual(t, len(book.Notes), 2, "Book should have one note")
|
||||
testutils.AssertNotEqual(t, book.Notes[0].UUID, "", "Note should have UUID")
|
||||
testutils.AssertEqual(t, book.Notes[0].Content, "Booleans have toString()", "Note content mismatch")
|
||||
testutils.AssertNotEqual(t, book.Notes[1].UUID, "", "Note should have UUID")
|
||||
testutils.AssertEqual(t, book.Notes[1].Content, "foo", "Note content mismatch")
|
||||
testutils.AssertEqual(t, noteActionData.Content, "foo", "action data name mismatch")
|
||||
testutils.AssertNotEqual(t, noteActionData.NoteUUID, "", "action data note_uuid mismatch")
|
||||
testutils.AssertEqual(t, noteActionData.BookName, "js", "action data book_name mismatch")
|
||||
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertNotEqual(t, n1.UUID, "", "Note should have UUID")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")
|
||||
testutils.AssertEqual(t, n1.AddedOn, int64(1515199943), "Note added_on mismatch")
|
||||
testutils.AssertNotEqual(t, n2.UUID, "", "Note should have UUID")
|
||||
testutils.AssertEqual(t, n2.Content, "foo", "Note content mismatch")
|
||||
}
|
||||
|
||||
func TestEdit_ContentFlag(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("./tmp")
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
func TestEditNote_ContentFlag(t *testing.T) {
|
||||
// Set up
|
||||
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// init files by running root command
|
||||
runDnoteCmd(ctx)
|
||||
testutils.WriteFile(ctx, "./testutils/fixtures/dnote2.json", "dnote")
|
||||
testutils.Setup4(t, ctx)
|
||||
|
||||
// Execute
|
||||
runDnoteCmd(ctx, "edit", "js", "1", "-c", "foo bar")
|
||||
testutils.RunDnoteCmd(t, ctx, binaryName, "edit", "js", "2", "-c", "foo bar")
|
||||
|
||||
// Test
|
||||
dnote, err := core.GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
actionSlice, err := core.ReadActionLog(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to read actions"))
|
||||
}
|
||||
db := ctx.DB
|
||||
|
||||
book := dnote["js"]
|
||||
action := actionSlice[0]
|
||||
var actionCount, noteCount, bookCount int
|
||||
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
|
||||
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
|
||||
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
|
||||
testutils.AssertEqualf(t, noteCount, 2, "note count mismatch")
|
||||
|
||||
var n1, n2 infra.Note
|
||||
testutils.MustScan(t, "getting n1",
|
||||
db.QueryRow("SELECT uuid, content, added_on FROM notes where book_uuid = ? AND uuid = ?", "js-book-uuid", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content, &n1.AddedOn)
|
||||
testutils.MustScan(t, "getting n2",
|
||||
db.QueryRow("SELECT uuid, content, added_on FROM notes where book_uuid = ? AND uuid = ?", "js-book-uuid", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n2.UUID, &n2.Content, &n2.AddedOn)
|
||||
var noteAction actions.Action
|
||||
testutils.MustScan(t, "getting note action",
|
||||
db.QueryRow("SELECT data, type, schema FROM actions where type = ?", actions.ActionEditNote),
|
||||
¬eAction.Data, ¬eAction.Type, ¬eAction.Schema)
|
||||
|
||||
var actionData actions.EditNoteDataV2
|
||||
err = json.Unmarshal(action.Data, &actionData)
|
||||
if err != nil {
|
||||
if err := json.Unmarshal(noteAction.Data, &actionData); err != nil {
|
||||
log.Fatalf("Failed to unmarshal the action data: %s", err)
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, len(actionSlice), 1, "There should be 1 action")
|
||||
testutils.AssertEqual(t, action.Type, actions.ActionEditNote, "action type mismatch")
|
||||
testutils.AssertEqual(t, action.Schema, 2, "action schema mismatch")
|
||||
testutils.AssertEqual(t, noteAction.Type, actions.ActionEditNote, "action type mismatch")
|
||||
testutils.AssertEqual(t, noteAction.Schema, 2, "action schema mismatch")
|
||||
testutils.AssertEqual(t, *actionData.Content, "foo bar", "action data name mismatch")
|
||||
testutils.AssertEqual(t, actionData.FromBook, "js", "action data from_book mismatch")
|
||||
if actionData.ToBook != nil {
|
||||
t.Errorf("action data to_book mismatch. Expected %+v. Got %+v", nil, actionData.ToBook)
|
||||
}
|
||||
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuis mismatch")
|
||||
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertEqual(t, len(book.Notes), 2, "Book should have one note")
|
||||
testutils.AssertEqual(t, book.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "Note should have UUID")
|
||||
testutils.AssertEqual(t, book.Notes[0].Content, "Booleans have toString()", "Note content mismatch")
|
||||
testutils.AssertEqual(t, book.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "Note should have UUID")
|
||||
testutils.AssertEqual(t, book.Notes[1].Content, "foo bar", "Note content mismatch")
|
||||
testutils.AssertNotEqual(t, book.Notes[1].EditedOn, int64(0), "Note edited_on mismatch")
|
||||
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "Note should have UUID")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")
|
||||
testutils.AssertEqual(t, n2.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "Note should have UUID")
|
||||
testutils.AssertEqual(t, n2.Content, "foo bar", "Note content mismatch")
|
||||
testutils.AssertNotEqual(t, n2.EditedOn, 0, "Note edited_on mismatch")
|
||||
}
|
||||
|
||||
func TestRemoveNote(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("./tmp")
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
// Set up
|
||||
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// init files by running root command
|
||||
runDnoteCmd(ctx)
|
||||
testutils.WriteFile(ctx, "./testutils/fixtures/dnote3.json", "dnote")
|
||||
testutils.Setup2(t, ctx)
|
||||
|
||||
// Execute
|
||||
cmd, stderr, err := newDnoteCmd(ctx, "remove", "js", "0")
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to get command"))
|
||||
}
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to get stdin %s"))
|
||||
}
|
||||
defer stdin.Close()
|
||||
|
||||
// Start the program
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to start command"))
|
||||
}
|
||||
|
||||
// confirm
|
||||
_, err = io.WriteString(stdin, "y\n")
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to write to stdin"))
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "Failed to run command %s", stderr.String()))
|
||||
}
|
||||
testutils.WaitDnoteCmd(t, ctx, testutils.UserConfirm, binaryName, "remove", "js", "1")
|
||||
|
||||
// Test
|
||||
dnote, err := core.GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
actionSlice, err := core.ReadActionLog(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to read actions"))
|
||||
}
|
||||
db := ctx.DB
|
||||
|
||||
if len(actionSlice) != 1 {
|
||||
t.Fatalf("action log length mismatch. got %d", len(actionSlice))
|
||||
}
|
||||
var actionCount, noteCount, bookCount, jsNoteCount, linuxNoteCount int
|
||||
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
testutils.MustScan(t, "counting js notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
|
||||
testutils.MustScan(t, "counting linux notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
|
||||
|
||||
book := dnote["js"]
|
||||
otherBook := dnote["linux"]
|
||||
action := actionSlice[0]
|
||||
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
|
||||
testutils.AssertEqualf(t, bookCount, 2, "book count mismatch")
|
||||
testutils.AssertEqualf(t, noteCount, 2, "note count mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, 1, "Book should have one note")
|
||||
testutils.AssertEqual(t, linuxNoteCount, 1, "Other book should have one note")
|
||||
|
||||
var b1, b2 infra.Book
|
||||
var n1 infra.Note
|
||||
testutils.MustScan(t, "getting b1",
|
||||
db.QueryRow("SELECT label FROM books WHERE uuid = ?", "js-book-uuid"),
|
||||
&b1.Name)
|
||||
testutils.MustScan(t, "getting b2",
|
||||
db.QueryRow("SELECT label FROM books WHERE uuid = ?", "linux-book-uuid"),
|
||||
&b2.Name)
|
||||
testutils.MustScan(t, "getting n1",
|
||||
db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE book_uuid = ? AND id = ?", "js-book-uuid", 2),
|
||||
&n1.UUID, &n1.Content, &n1.AddedOn)
|
||||
|
||||
var noteAction actions.Action
|
||||
testutils.MustScan(t, "getting note action",
|
||||
db.QueryRow("SELECT type, schema, data FROM actions WHERE type = ?", actions.ActionRemoveNote), ¬eAction.Type, ¬eAction.Schema, ¬eAction.Data)
|
||||
|
||||
var actionData actions.RemoveNoteDataV1
|
||||
err = json.Unmarshal(action.Data, &actionData)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to unmarshal the action data: %s", err)
|
||||
if err := json.Unmarshal(noteAction.Data, &actionData); err != nil {
|
||||
log.Fatalf("unmarshalling the action data: %s", err)
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, len(actionSlice), 1, "There should be 1 action")
|
||||
testutils.AssertEqual(t, action.Type, actions.ActionRemoveNote, "action type mismatch")
|
||||
testutils.AssertEqual(t, actionData.NoteUUID, "43827b9a-c2b0-4c06-a290-97991c896653", "action data note_uuid mismatch")
|
||||
testutils.AssertEqual(t, b1.Name, "js", "b1 label mismatch")
|
||||
testutils.AssertEqual(t, b2.Name, "linux", "b2 label mismatch")
|
||||
testutils.AssertEqual(t, noteAction.Schema, 1, "action schema mismatch")
|
||||
testutils.AssertEqual(t, noteAction.Type, actions.ActionRemoveNote, "action type mismatch")
|
||||
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuid mismatch")
|
||||
testutils.AssertEqual(t, actionData.BookName, "js", "action data book_name mismatch")
|
||||
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertEqual(t, len(book.Notes), 1, "Book should have one note")
|
||||
testutils.AssertEqual(t, len(otherBook.Notes), 1, "Other book should have one note")
|
||||
testutils.AssertEqual(t, book.Notes[0].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "Note should have UUID")
|
||||
testutils.AssertEqual(t, book.Notes[0].Content, "Date object implements mathematical comparisons", "Note content mismatch")
|
||||
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "Note should have UUID")
|
||||
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")
|
||||
}
|
||||
|
||||
func TestRemoveBook(t *testing.T) {
|
||||
// Setup
|
||||
ctx := testutils.InitCtx("./tmp")
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
// Set up
|
||||
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// init files by running root command
|
||||
runDnoteCmd(ctx)
|
||||
testutils.WriteFile(ctx, "./testutils/fixtures/dnote3.json", "dnote")
|
||||
testutils.Setup2(t, ctx)
|
||||
|
||||
// Execute
|
||||
cmd, stderr, err := newDnoteCmd(ctx, "remove", "-b", "js")
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to get command"))
|
||||
}
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to get stdin %s"))
|
||||
}
|
||||
defer stdin.Close()
|
||||
|
||||
// Start the program
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to start command"))
|
||||
}
|
||||
|
||||
// confirm
|
||||
_, err = io.WriteString(stdin, "y\n")
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to write to stdin"))
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "Failed to run command %s", stderr.String()))
|
||||
}
|
||||
testutils.WaitDnoteCmd(t, ctx, testutils.UserConfirm, binaryName, "remove", "-b", "js")
|
||||
|
||||
// Test
|
||||
dnote, err := core.GetDnote(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to get dnote"))
|
||||
}
|
||||
actionSlice, err := core.ReadActionLog(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to read actions"))
|
||||
}
|
||||
db := ctx.DB
|
||||
|
||||
if len(actionSlice) != 1 {
|
||||
t.Fatalf("action log length mismatch. got %d", len(actionSlice))
|
||||
}
|
||||
var actionCount, noteCount, bookCount, jsNoteCount, linuxNoteCount int
|
||||
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
|
||||
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
|
||||
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), ¬eCount)
|
||||
testutils.MustScan(t, "counting js notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
|
||||
testutils.MustScan(t, "counting linux notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
|
||||
|
||||
book := dnote["linux"]
|
||||
action := actionSlice[0]
|
||||
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
|
||||
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
|
||||
testutils.AssertEqualf(t, noteCount, 1, "note count mismatch")
|
||||
testutils.AssertEqual(t, jsNoteCount, 0, "some notes in book were not deleted")
|
||||
testutils.AssertEqual(t, linuxNoteCount, 1, "Other book should have one note")
|
||||
|
||||
var b1 infra.Book
|
||||
testutils.MustScan(t, "getting b1",
|
||||
db.QueryRow("SELECT label FROM books WHERE uuid = ?", "linux-book-uuid"),
|
||||
&b1.Name)
|
||||
|
||||
var action actions.Action
|
||||
testutils.MustScan(t, "getting an action",
|
||||
db.QueryRow("SELECT type, schema, data FROM actions WHERE type = ?", actions.ActionRemoveBook), &action.Type, &action.Schema, &action.Data)
|
||||
|
||||
var actionData actions.RemoveBookDataV1
|
||||
err = json.Unmarshal(action.Data, &actionData)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to unmarshal the action data: %s", err)
|
||||
if err := json.Unmarshal(action.Data, &actionData); err != nil {
|
||||
log.Fatalf("unmarshalling the action data: %s", err)
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, len(actionSlice), 1, "There should be 1 action")
|
||||
testutils.AssertEqual(t, action.Type, actions.ActionRemoveBook, "action type mismatch")
|
||||
testutils.AssertEqual(t, actionData.BookName, "js", "action data name mismatch")
|
||||
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
|
||||
testutils.AssertEqual(t, len(dnote), 1, "There should be 1 book")
|
||||
testutils.AssertEqual(t, book.Name, "linux", "Remaining book name mismatch")
|
||||
testutils.AssertEqual(t, len(book.Notes), 1, "Remaining book should have one note")
|
||||
testutils.AssertEqual(t, b1.Name, "linux", "Remaining book name mismatch")
|
||||
}
|
||||
|
|
|
|||
2
migrate/fixtures/8-actions.json
Normal file
2
migrate/fixtures/8-actions.json
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[{"uuid":"6145c1b7-f286-4d9f-b0f6-00d274baefc6","schema":1,"type":"add_book","data":{"book_name":"js"},"timestamp":1536977229},{"uuid":"c048a56b-179c-4f31-9995-81e9b32b7dd6","schema":2,"type":"add_note","data":{"note_uuid":"35cbcab1-6a2a-4cc8-97e0-e73bbbd54626","book_name":"js","content":"js test 1","public":false},"timestamp":1536977229},{"uuid":"f557ef48-c304-47dc-adfb-46b7306e701f","schema":2,"type":"add_note","data":{"note_uuid":"7c1fcfb2-de8b-4350-88f0-fb3cbaf6630a","book_name":"js","content":"js test 2","public":false},"timestamp":1536977230},{"uuid":"8d79db34-343d-4331-ae5b-24743f17ca7f","schema":2,"type":"add_note","data":{"note_uuid":"b23a88ba-b291-4294-9795-86b394db5dcf","book_name":"js","content":"js test 3","public":false},"timestamp":1536977234},{"uuid":"b9c1ed4a-e6b3-41f2-983b-593ec7b8b7a1","schema":1,"type":"add_book","data":{"book_name":"css"},"timestamp":1536977237},{"uuid":"06ed7ef0-f171-4bd7-ae8e-97b5d06a4c49","schema":2,"type":"add_note","data":{"note_uuid":"d69edb54-5b31-4cdd-a4a5-34f0a0bfa153","book_name":"css","content":"js test 3","public":false},"timestamp":1536977237},{"uuid":"7f173cef-1688-4177-a373-145fcd822b2f","schema":2,"type":"edit_note","data":{"note_uuid":"d69edb54-5b31-4cdd-a4a5-34f0a0bfa153","from_book":"css","to_book":null,"content":"css test 1","public":null},"timestamp":1536977253},{"uuid":"64352e08-aa7a-45f4-b760-b3f38b5e11fa","schema":1,"type":"add_book","data":{"book_name":"sql"},"timestamp":1536977261},{"uuid":"82e20a12-bda8-45f7-ac42-b453b6daa5ec","schema":2,"type":"add_note","data":{"note_uuid":"2f47d390-685b-4b84-89ac-704c6fb8d3fb","book_name":"sql","content":"blah","public":false},"timestamp":1536977261},{"uuid":"a29055f4-ace4-44fd-8800-3396edbccaef","schema":1,"type":"remove_book","data":{"book_name":"sql"},"timestamp":1536977268},{"uuid":"871a5562-1bd0-43c1-b550-5bbb727ac7c4","schema":1,"type":"remove_note","data":{"note_uuid":"b23a88ba-b291-4294-9795-86b394db5dcf","book_name":"js"},"timestamp":1536977274}]
|
||||
|
||||
33
migrate/fixtures/8-dnote.json
Normal file
33
migrate/fixtures/8-dnote.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"css": {
|
||||
"name": "css",
|
||||
"notes": [
|
||||
{
|
||||
"uuid": "d69edb54-5b31-4cdd-a4a5-34f0a0bfa153",
|
||||
"content": "css test 1",
|
||||
"added_on": 1536977237,
|
||||
"edited_on": 1536977253,
|
||||
"public": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"js": {
|
||||
"name": "js",
|
||||
"notes": [
|
||||
{
|
||||
"uuid": "35cbcab1-6a2a-4cc8-97e0-e73bbbd54626",
|
||||
"content": "js test 1",
|
||||
"added_on": 1536977229,
|
||||
"edited_on": 0,
|
||||
"public": false
|
||||
},
|
||||
{
|
||||
"uuid": "7c1fcfb2-de8b-4350-88f0-fb3cbaf6630a",
|
||||
"content": "js test 2",
|
||||
"added_on": 1536977230,
|
||||
"edited_on": 0,
|
||||
"public": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
3
migrate/fixtures/8-dnoterc.yaml
Normal file
3
migrate/fixtures/8-dnoterc.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
editor: vim
|
||||
apikey: ""
|
||||
|
||||
1
migrate/fixtures/8-schema.yaml
Normal file
1
migrate/fixtures/8-schema.yaml
Normal file
|
|
@ -0,0 +1 @@
|
|||
current_version: 7
|
||||
3
migrate/fixtures/8-timestamps.yaml
Normal file
3
migrate/fixtures/8-timestamps.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
last_upgrade: 1536977220
|
||||
bookmark: 9
|
||||
last_action: 1536977274
|
||||
950
migrate/legacy.go
Normal file
950
migrate/legacy.go
Normal file
|
|
@ -0,0 +1,950 @@
|
|||
// Package migrate provides migration logic for both sqlite and
|
||||
// legacy JSON-based notes used until v0.4.x releases
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/satori/go.uuid"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
schemaFilename = "schema"
|
||||
backupDirName = ".dnote-bak"
|
||||
)
|
||||
|
||||
// migration IDs
|
||||
const (
|
||||
_ = iota
|
||||
legacyMigrationV1
|
||||
legacyMigrationV2
|
||||
legacyMigrationV3
|
||||
legacyMigrationV4
|
||||
legacyMigrationV5
|
||||
legacyMigrationV6
|
||||
legacyMigrationV7
|
||||
legacyMigrationV8
|
||||
)
|
||||
|
||||
var migrationSequence = []int{
|
||||
legacyMigrationV1,
|
||||
legacyMigrationV2,
|
||||
legacyMigrationV3,
|
||||
legacyMigrationV4,
|
||||
legacyMigrationV5,
|
||||
legacyMigrationV6,
|
||||
legacyMigrationV7,
|
||||
legacyMigrationV8,
|
||||
}
|
||||
|
||||
type schema struct {
|
||||
currentVersion int `yaml:"current_version"`
|
||||
}
|
||||
|
||||
func makeSchema(complete bool) schema {
|
||||
s := schema{}
|
||||
|
||||
var currentVersion int
|
||||
if complete {
|
||||
currentVersion = len(migrationSequence)
|
||||
}
|
||||
|
||||
s.currentVersion = currentVersion
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Legacy performs migration on JSON-based dnote if necessary
|
||||
func Legacy(ctx infra.DnoteCtx) error {
|
||||
// If schema does not exist, no need run a legacy migration
|
||||
schemaPath := getSchemaPath(ctx)
|
||||
if ok := utils.FileExists(schemaPath); !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
unrunMigrations, err := getUnrunMigrations(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get unrun migrations")
|
||||
}
|
||||
|
||||
for _, mid := range unrunMigrations {
|
||||
if err := performMigration(ctx, mid); err != nil {
|
||||
return errors.Wrapf(err, "running migration #%d", mid)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// performMigration backs up current .dnote data, performs migration, and
|
||||
// restores or cleans backups depending on if there is an error
|
||||
func performMigration(ctx infra.DnoteCtx, migrationID int) error {
|
||||
// legacyMigrationV8 is the final migration of the legacy JSON Dnote migration
|
||||
// migrate to sqlite and return
|
||||
if migrationID == legacyMigrationV8 {
|
||||
if err := migrateToV8(ctx); err != nil {
|
||||
return errors.Wrap(err, "migrating to sqlite")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := backupDnoteDir(ctx); err != nil {
|
||||
return errors.Wrap(err, "Failed to back up dnote directory")
|
||||
}
|
||||
|
||||
var migrationError error
|
||||
|
||||
switch migrationID {
|
||||
case legacyMigrationV1:
|
||||
migrationError = migrateToV1(ctx)
|
||||
case legacyMigrationV2:
|
||||
migrationError = migrateToV2(ctx)
|
||||
case legacyMigrationV3:
|
||||
migrationError = migrateToV3(ctx)
|
||||
case legacyMigrationV4:
|
||||
migrationError = migrateToV4(ctx)
|
||||
case legacyMigrationV5:
|
||||
migrationError = migrateToV5(ctx)
|
||||
case legacyMigrationV6:
|
||||
migrationError = migrateToV6(ctx)
|
||||
case legacyMigrationV7:
|
||||
migrationError = migrateToV7(ctx)
|
||||
default:
|
||||
return errors.Errorf("Unrecognized migration id %d", migrationID)
|
||||
}
|
||||
|
||||
if migrationError != nil {
|
||||
if err := restoreBackup(ctx); err != nil {
|
||||
panic(errors.Wrap(err, "Failed to restore backup for a failed migration"))
|
||||
}
|
||||
|
||||
return errors.Wrapf(migrationError, "Failed to perform migration #%d", migrationID)
|
||||
}
|
||||
|
||||
if err := clearBackup(ctx); err != nil {
|
||||
return errors.Wrap(err, "Failed to clear backup")
|
||||
}
|
||||
|
||||
if err := updateSchemaVersion(ctx, migrationID); err != nil {
|
||||
return errors.Wrap(err, "Failed to update schema version")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// backupDnoteDir backs up the dnote directory to a temporary backup directory
|
||||
func backupDnoteDir(ctx infra.DnoteCtx) error {
|
||||
srcPath := fmt.Sprintf("%s/.dnote", ctx.HomeDir)
|
||||
tmpPath := fmt.Sprintf("%s/%s", ctx.HomeDir, backupDirName)
|
||||
|
||||
if err := utils.CopyDir(srcPath, tmpPath); err != nil {
|
||||
return errors.Wrap(err, "Failed to copy the .dnote directory")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreBackup(ctx infra.DnoteCtx) error {
|
||||
var err error
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.Printf(`Failed to restore backup for a failed migration.
|
||||
Don't worry. Your data is still intact in the backup directory.
|
||||
Get help on https://github.com/dnote/cli/issues`)
|
||||
}
|
||||
}()
|
||||
|
||||
srcPath := fmt.Sprintf("%s/.dnote", ctx.HomeDir)
|
||||
backupPath := fmt.Sprintf("%s/%s", ctx.HomeDir, backupDirName)
|
||||
|
||||
if err = os.RemoveAll(srcPath); err != nil {
|
||||
return errors.Wrapf(err, "Failed to clear current dnote data at %s", backupPath)
|
||||
}
|
||||
|
||||
if err = os.Rename(backupPath, srcPath); err != nil {
|
||||
return errors.Wrap(err, `Failed to copy backup data to the original directory.`)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func clearBackup(ctx infra.DnoteCtx) error {
|
||||
backupPath := fmt.Sprintf("%s/%s", ctx.HomeDir, backupDirName)
|
||||
|
||||
if err := os.RemoveAll(backupPath); err != nil {
|
||||
return errors.Wrapf(err, "Failed to remove backup at %s", backupPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSchemaPath returns the path to the file containing schema info
|
||||
func getSchemaPath(ctx infra.DnoteCtx) string {
|
||||
return fmt.Sprintf("%s/%s", ctx.DnoteDir, schemaFilename)
|
||||
}
|
||||
|
||||
// initSchemaFile creates a migration file
|
||||
func initSchemaFile(ctx infra.DnoteCtx, pristine bool) error {
|
||||
path := getSchemaPath(ctx)
|
||||
if utils.FileExists(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := makeSchema(pristine)
|
||||
err := writeSchema(ctx, s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write schema")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSchema(ctx infra.DnoteCtx) (schema, error) {
|
||||
var ret schema
|
||||
|
||||
path := getSchemaPath(ctx)
|
||||
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "Failed to read schema file")
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(b, &ret)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "Failed to unmarshal the schema JSON")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func writeSchema(ctx infra.DnoteCtx, s schema) error {
|
||||
path := getSchemaPath(ctx)
|
||||
d, err := yaml.Marshal(&s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal schema into yaml")
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, d, 0644); err != nil {
|
||||
return errors.Wrap(err, "Failed to write schema file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUnrunMigrations(ctx infra.DnoteCtx) ([]int, error) {
|
||||
var ret []int
|
||||
|
||||
schema, err := readSchema(ctx)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "Failed to read schema")
|
||||
}
|
||||
|
||||
if schema.currentVersion == len(migrationSequence) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
nextVersion := schema.currentVersion
|
||||
ret = migrationSequence[nextVersion:]
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func updateSchemaVersion(ctx infra.DnoteCtx, mID int) error {
|
||||
s, err := readSchema(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read schema")
|
||||
}
|
||||
|
||||
s.currentVersion = mID
|
||||
|
||||
err = writeSchema(ctx, s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write schema")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/***** snapshots **/
|
||||
|
||||
// v2
|
||||
type migrateToV2PreNote struct {
|
||||
UID string
|
||||
Content string
|
||||
AddedOn int64
|
||||
}
|
||||
type migrateToV2PostNote struct {
|
||||
UUID string `json:"uuid"`
|
||||
Content string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"editd_on"`
|
||||
}
|
||||
type migrateToV2PreBook []migrateToV2PreNote
|
||||
type migrateToV2PostBook struct {
|
||||
Name string `json:"name"`
|
||||
Notes []migrateToV2PostNote `json:"notes"`
|
||||
}
|
||||
type migrateToV2PreDnote map[string]migrateToV2PreBook
|
||||
type migrateToV2PostDnote map[string]migrateToV2PostBook
|
||||
|
||||
//v3
|
||||
var (
|
||||
migrateToV3ActionAddNote = "add_note"
|
||||
migrateToV3ActionAddBook = "add_book"
|
||||
)
|
||||
|
||||
type migrateToV3Note struct {
|
||||
UUID string `json:"uuid"`
|
||||
Content string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
}
|
||||
type migrateToV3Book struct {
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Notes []migrateToV3Note `json:"notes"`
|
||||
}
|
||||
type migrateToV3Dnote map[string]migrateToV3Book
|
||||
type migrateToV3Action struct {
|
||||
Type string `json:"type"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// v4
|
||||
type migrateToV4PreConfig struct {
|
||||
Book string
|
||||
APIKey string
|
||||
}
|
||||
type migrateToV4PostConfig struct {
|
||||
Editor string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
// v5
|
||||
type migrateToV5AddNoteData struct {
|
||||
NoteUUID string `json:"note_uuid"`
|
||||
BookName string `json:"book_name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
type migrateToV5RemoveNoteData struct {
|
||||
NoteUUID string `json:"note_uuid"`
|
||||
BookName string `json:"book_name"`
|
||||
}
|
||||
type migrateToV5AddBookData struct {
|
||||
BookName string `json:"book_name"`
|
||||
}
|
||||
type migrateToV5RemoveBookData struct {
|
||||
BookName string `json:"book_name"`
|
||||
}
|
||||
type migrateToV5PreEditNoteData struct {
|
||||
NoteUUID string `json:"note_uuid"`
|
||||
BookName string `json:"book_name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
type migrateToV5PostEditNoteData struct {
|
||||
NoteUUID string `json:"note_uuid"`
|
||||
FromBook string `json:"from_book"`
|
||||
ToBook string `json:"to_book"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
type migrateToV5PreAction struct {
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
type migrateToV5PostAction struct {
|
||||
UUID string `json:"uuid"`
|
||||
Schema int `json:"schema"`
|
||||
Type string `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
var (
|
||||
migrateToV5ActionAddNote = "add_note"
|
||||
migrateToV5ActionRemoveNote = "remove_note"
|
||||
migrateToV5ActionEditNote = "edit_note"
|
||||
migrateToV5ActionAddBook = "add_book"
|
||||
migrateToV5ActionRemoveBook = "remove_book"
|
||||
)
|
||||
|
||||
// v6
|
||||
type migrateToV6PreNote struct {
|
||||
UUID string `json:"uuid"`
|
||||
Content string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
}
|
||||
type migrateToV6PostNote struct {
|
||||
UUID string `json:"uuid"`
|
||||
Content string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
// Make a pointer to test absent values
|
||||
Public *bool `json:"public"`
|
||||
}
|
||||
type migrateToV6PreBook struct {
|
||||
Name string `json:"name"`
|
||||
Notes []migrateToV6PreNote `json:"notes"`
|
||||
}
|
||||
type migrateToV6PostBook struct {
|
||||
Name string `json:"name"`
|
||||
Notes []migrateToV6PostNote `json:"notes"`
|
||||
}
|
||||
type migrateToV6PreDnote map[string]migrateToV6PreBook
|
||||
type migrateToV6PostDnote map[string]migrateToV6PostBook
|
||||
|
||||
// v7
|
||||
var migrateToV7ActionTypeEditNote = "edit_note"
|
||||
|
||||
type migrateToV7Action struct {
|
||||
UUID string `json:"uuid"`
|
||||
Schema int `json:"schema"`
|
||||
Type string `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
type migrateToV7EditNoteDataV1 struct {
|
||||
NoteUUID string `json:"note_uuid"`
|
||||
FromBook string `json:"from_book"`
|
||||
ToBook string `json:"to_book"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
type migrateToV7EditNoteDataV2 struct {
|
||||
NoteUUID string `json:"note_uuid"`
|
||||
FromBook string `json:"from_book"`
|
||||
ToBook *string `json:"to_book"`
|
||||
Content *string `json:"content"`
|
||||
Public *bool `json:"public"`
|
||||
}
|
||||
|
||||
// v8
|
||||
type migrateToV8Action struct {
|
||||
UUID string `json:"uuid"`
|
||||
Schema int `json:"schema"`
|
||||
Type string `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
type migrateToV8Note struct {
|
||||
UUID string `json:"uuid"`
|
||||
Content string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
// Make a pointer to test absent values
|
||||
Public *bool `json:"public"`
|
||||
}
|
||||
type migrateToV8Book struct {
|
||||
Name string `json:"name"`
|
||||
Notes []migrateToV8Note `json:"notes"`
|
||||
}
|
||||
type migrateToV8Dnote map[string]migrateToV8Book
|
||||
type migrateToV8Timestamp struct {
|
||||
LastUpgrade int64 `yaml:"last_upgrade"`
|
||||
Bookmark int `yaml:"bookmark"`
|
||||
LastAction int64 `yaml:"last_action"`
|
||||
}
|
||||
|
||||
var migrateToV8SystemKeyLastUpgrade = "last_upgrade"
|
||||
var migrateToV8SystemKeyLastAction = "last_action"
|
||||
var migrateToV8SystemKeyBookMark = "bookmark"
|
||||
|
||||
/***** migrations **/
|
||||
|
||||
// migrateToV1 deletes YAML archive if exists
|
||||
func migrateToV1(ctx infra.DnoteCtx) error {
|
||||
yamlPath := fmt.Sprintf("%s/%s", ctx.HomeDir, ".dnote-yaml-archived")
|
||||
if !utils.FileExists(yamlPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.Remove(yamlPath); err != nil {
|
||||
return errors.Wrap(err, "Failed to delete .dnote archive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateToV2(ctx infra.DnoteCtx) error {
|
||||
notePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
|
||||
|
||||
b, err := ioutil.ReadFile(notePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the note file")
|
||||
}
|
||||
|
||||
var preDnote migrateToV2PreDnote
|
||||
postDnote := migrateToV2PostDnote{}
|
||||
|
||||
err = json.Unmarshal(b, &preDnote)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
|
||||
}
|
||||
|
||||
for bookName, book := range preDnote {
|
||||
var notes = make([]migrateToV2PostNote, 0, len(book))
|
||||
for _, note := range book {
|
||||
newNote := migrateToV2PostNote{
|
||||
UUID: uuid.NewV4().String(),
|
||||
Content: note.Content,
|
||||
AddedOn: note.AddedOn,
|
||||
EditedOn: 0,
|
||||
}
|
||||
|
||||
notes = append(notes, newNote)
|
||||
}
|
||||
|
||||
b := migrateToV2PostBook{
|
||||
Name: bookName,
|
||||
Notes: notes,
|
||||
}
|
||||
|
||||
postDnote[bookName] = b
|
||||
}
|
||||
|
||||
d, err := json.MarshalIndent(postDnote, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal new dnote into JSON")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(notePath, d, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write the new dnote into the file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateToV3 generates actions for existing dnote
|
||||
func migrateToV3(ctx infra.DnoteCtx) error {
|
||||
notePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
|
||||
actionsPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
|
||||
|
||||
b, err := ioutil.ReadFile(notePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the note file")
|
||||
}
|
||||
|
||||
var dnote migrateToV3Dnote
|
||||
|
||||
err = json.Unmarshal(b, &dnote)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
|
||||
}
|
||||
|
||||
var actions []migrateToV3Action
|
||||
|
||||
for bookName, book := range dnote {
|
||||
// Find the minimum added_on timestamp from the notes that belong to the book
|
||||
// to give timstamp to the add_book action.
|
||||
// Logically add_book must have happened no later than the first add_note
|
||||
// to the book in order for sync to work.
|
||||
minTs := time.Now().Unix()
|
||||
for _, note := range book.Notes {
|
||||
if note.AddedOn < minTs {
|
||||
minTs = note.AddedOn
|
||||
}
|
||||
}
|
||||
|
||||
action := migrateToV3Action{
|
||||
Type: migrateToV3ActionAddBook,
|
||||
Data: map[string]interface{}{
|
||||
"book_name": bookName,
|
||||
},
|
||||
Timestamp: minTs,
|
||||
}
|
||||
actions = append(actions, action)
|
||||
|
||||
for _, note := range book.Notes {
|
||||
action := migrateToV3Action{
|
||||
Type: migrateToV3ActionAddNote,
|
||||
Data: map[string]interface{}{
|
||||
"note_uuid": note.UUID,
|
||||
"book_name": book.Name,
|
||||
"content": note.Content,
|
||||
},
|
||||
Timestamp: note.AddedOn,
|
||||
}
|
||||
actions = append(actions, action)
|
||||
}
|
||||
}
|
||||
|
||||
a, err := json.Marshal(actions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal actions into JSON")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(actionsPath, a, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write the actions into a file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEditorCommand() string {
|
||||
editor := os.Getenv("EDITOR")
|
||||
|
||||
switch editor {
|
||||
case "atom":
|
||||
return "atom -w"
|
||||
case "subl":
|
||||
return "subl -n -w"
|
||||
case "mate":
|
||||
return "mate -w"
|
||||
case "vim":
|
||||
return "vim"
|
||||
case "nano":
|
||||
return "nano"
|
||||
case "emacs":
|
||||
return "emacs"
|
||||
default:
|
||||
return "vi"
|
||||
}
|
||||
}
|
||||
|
||||
func migrateToV4(ctx infra.DnoteCtx) error {
|
||||
configPath := fmt.Sprintf("%s/dnoterc", ctx.DnoteDir)
|
||||
|
||||
b, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the config file")
|
||||
}
|
||||
|
||||
var preConfig migrateToV4PreConfig
|
||||
err = yaml.Unmarshal(b, &preConfig)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to unmarshal existing config into JSON")
|
||||
}
|
||||
|
||||
postConfig := migrateToV4PostConfig{
|
||||
APIKey: preConfig.APIKey,
|
||||
Editor: getEditorCommand(),
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(postConfig)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal config into JSON")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(configPath, data, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write the config into a file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateToV5 migrates actions
|
||||
func migrateToV5(ctx infra.DnoteCtx) error {
|
||||
actionsPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
|
||||
|
||||
b, err := ioutil.ReadFile(actionsPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading the actions file")
|
||||
}
|
||||
|
||||
var actions []migrateToV5PreAction
|
||||
err = json.Unmarshal(b, &actions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarshalling actions from JSON")
|
||||
}
|
||||
|
||||
result := []migrateToV5PostAction{}
|
||||
|
||||
for _, action := range actions {
|
||||
var data json.RawMessage
|
||||
|
||||
switch action.Type {
|
||||
case migrateToV5ActionEditNote:
|
||||
var oldData migrateToV5PreEditNoteData
|
||||
if err = json.Unmarshal(action.Data, &oldData); err != nil {
|
||||
return errors.Wrapf(err, "unmarshalling old data of an edit note action %d", action.ID)
|
||||
}
|
||||
|
||||
migratedData := migrateToV5PostEditNoteData{
|
||||
NoteUUID: oldData.NoteUUID,
|
||||
FromBook: oldData.BookName,
|
||||
Content: oldData.Content,
|
||||
}
|
||||
b, err = json.Marshal(migratedData)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling data")
|
||||
}
|
||||
|
||||
data = b
|
||||
default:
|
||||
data = action.Data
|
||||
}
|
||||
|
||||
migrated := migrateToV5PostAction{
|
||||
UUID: uuid.NewV4().String(),
|
||||
Schema: 1,
|
||||
Type: action.Type,
|
||||
Data: data,
|
||||
Timestamp: action.Timestamp,
|
||||
}
|
||||
|
||||
result = append(result, migrated)
|
||||
}
|
||||
|
||||
a, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling result into JSON")
|
||||
}
|
||||
err = ioutil.WriteFile(actionsPath, a, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "writing the result into a file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateToV6 adds a 'public' field to notes
|
||||
func migrateToV6(ctx infra.DnoteCtx) error {
|
||||
notePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
|
||||
|
||||
b, err := ioutil.ReadFile(notePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the note file")
|
||||
}
|
||||
|
||||
var preDnote migrateToV6PreDnote
|
||||
postDnote := migrateToV6PostDnote{}
|
||||
|
||||
err = json.Unmarshal(b, &preDnote)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
|
||||
}
|
||||
|
||||
for bookName, book := range preDnote {
|
||||
var notes = make([]migrateToV6PostNote, 0, len(book.Notes))
|
||||
public := false
|
||||
for _, note := range book.Notes {
|
||||
newNote := migrateToV6PostNote{
|
||||
UUID: note.UUID,
|
||||
Content: note.Content,
|
||||
AddedOn: note.AddedOn,
|
||||
EditedOn: note.EditedOn,
|
||||
Public: &public,
|
||||
}
|
||||
|
||||
notes = append(notes, newNote)
|
||||
}
|
||||
|
||||
b := migrateToV6PostBook{
|
||||
Name: bookName,
|
||||
Notes: notes,
|
||||
}
|
||||
|
||||
postDnote[bookName] = b
|
||||
}
|
||||
|
||||
d, err := json.MarshalIndent(postDnote, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal new dnote into JSON")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(notePath, d, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write the new dnote into the file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateToV7 migrates data of edit_note action to the proper version which is
|
||||
// EditNoteDataV2. Due to a bug, edit logged actions with schema version '2'
|
||||
// but with a data of EditNoteDataV1. https://github.com/dnote/cli/issues/107
|
||||
func migrateToV7(ctx infra.DnoteCtx) error {
|
||||
actionPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
|
||||
|
||||
b, err := ioutil.ReadFile(actionPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading actions file")
|
||||
}
|
||||
|
||||
var preActions []migrateToV7Action
|
||||
postActions := []migrateToV7Action{}
|
||||
err = json.Unmarshal(b, &preActions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarshalling existing actions")
|
||||
}
|
||||
|
||||
for _, action := range preActions {
|
||||
var newAction migrateToV7Action
|
||||
|
||||
if action.Type == migrateToV7ActionTypeEditNote {
|
||||
var oldData migrateToV7EditNoteDataV1
|
||||
if e := json.Unmarshal(action.Data, &oldData); e != nil {
|
||||
return errors.Wrapf(e, "unmarshalling data of action with uuid %s", action.Data)
|
||||
}
|
||||
|
||||
newData := migrateToV7EditNoteDataV2{
|
||||
NoteUUID: oldData.NoteUUID,
|
||||
FromBook: oldData.FromBook,
|
||||
ToBook: nil,
|
||||
Content: &oldData.Content,
|
||||
Public: nil,
|
||||
}
|
||||
d, e := json.Marshal(newData)
|
||||
if e != nil {
|
||||
return errors.Wrapf(e, "marshalling new data of action with uuid %s", action.Data)
|
||||
}
|
||||
|
||||
newAction = migrateToV7Action{
|
||||
UUID: action.UUID,
|
||||
Schema: action.Schema,
|
||||
Type: action.Type,
|
||||
Timestamp: action.Timestamp,
|
||||
Data: d,
|
||||
}
|
||||
} else {
|
||||
newAction = action
|
||||
}
|
||||
|
||||
postActions = append(postActions, newAction)
|
||||
}
|
||||
|
||||
d, err := json.Marshal(postActions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling new actions")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(actionPath, d, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "writing new actions to a file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateToV8 migrates dnote data to sqlite database
|
||||
func migrateToV8(ctx infra.DnoteCtx) error {
|
||||
tx, err := ctx.DB.Begin()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
|
||||
// 1. Migrate the the dnote file
|
||||
dnoteFilePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
|
||||
b, err := ioutil.ReadFile(dnoteFilePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading the notes")
|
||||
}
|
||||
|
||||
var dnote migrateToV8Dnote
|
||||
err = json.Unmarshal(b, &dnote)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarshalling notes to JSON")
|
||||
}
|
||||
|
||||
for bookName, book := range dnote {
|
||||
bookUUID := uuid.NewV4().String()
|
||||
_, err = tx.Exec(`INSERT INTO books (uuid, label) VALUES (?, ?)`, bookUUID, bookName)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrapf(err, "inserting book %s", book.Name)
|
||||
}
|
||||
|
||||
for _, note := range book.Notes {
|
||||
_, err = tx.Exec(`INSERT INTO notes
|
||||
(uuid, book_uuid, content, added_on, edited_on, public)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, note.UUID, bookUUID, note.Content, note.AddedOn, note.EditedOn, note.Public)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrapf(err, "inserting the note %s", note.UUID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Migrate the actions file
|
||||
actionsPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
|
||||
b, err = ioutil.ReadFile(actionsPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading the actions")
|
||||
}
|
||||
|
||||
var actions []migrateToV8Action
|
||||
err = json.Unmarshal(b, &actions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarshalling actions from JSON")
|
||||
}
|
||||
|
||||
for _, action := range actions {
|
||||
_, err = tx.Exec(`INSERT INTO actions
|
||||
(uuid, schema, type, data, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, action.UUID, action.Schema, action.Type, action.Data, action.Timestamp)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrapf(err, "inserting the action %s", action.UUID)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Migrate the timestamps file
|
||||
timestampsPath := fmt.Sprintf("%s/timestamps", ctx.DnoteDir)
|
||||
b, err = ioutil.ReadFile(timestampsPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading the timestamps")
|
||||
}
|
||||
|
||||
var timestamp migrateToV8Timestamp
|
||||
err = yaml.Unmarshal(b, ×tamp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarshalling timestamps from YAML")
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`INSERT INTO system (key, value) VALUES (?, ?)`,
|
||||
migrateToV8SystemKeyLastUpgrade, timestamp.LastUpgrade)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "inserting the last_upgrade value")
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO system (key, value) VALUES (?, ?)`,
|
||||
migrateToV8SystemKeyLastAction, timestamp.LastAction)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "inserting the last_action value")
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO system (key, value) VALUES (?, ?)`,
|
||||
migrateToV8SystemKeyBookMark, timestamp.Bookmark)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "inserting the bookmark value")
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
if err := os.RemoveAll(dnoteFilePath); err != nil {
|
||||
return errors.Wrap(err, "removing the old dnote file")
|
||||
}
|
||||
if err := os.RemoveAll(actionsPath); err != nil {
|
||||
return errors.Wrap(err, "removing the actions file")
|
||||
}
|
||||
if err := os.RemoveAll(timestampsPath); err != nil {
|
||||
return errors.Wrap(err, "removing the timestamps file")
|
||||
}
|
||||
schemaPath := fmt.Sprintf("%s/schema", ctx.DnoteDir)
|
||||
if err := os.RemoveAll(schemaPath); err != nil {
|
||||
return errors.Wrap(err, "removing the schema file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
590
migrate/legacy_test.go
Normal file
590
migrate/legacy_test.go
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
package migrate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/cli/testutils"
|
||||
"github.com/dnote/cli/utils"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestMigrateToV1(t *testing.T) {
|
||||
|
||||
t.Run("yaml exists", func(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
yamlPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-yaml-archived"))
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to get absolute YAML path").Error())
|
||||
}
|
||||
ioutil.WriteFile(yamlPath, []byte{}, 0644)
|
||||
|
||||
// execute
|
||||
if err := migrateToV1(ctx); err != nil {
|
||||
t.Fatal(errors.Wrapf(err, "Failed to migrate").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
if utils.FileExists(yamlPath) {
|
||||
t.Fatal("YAML archive file has not been deleted")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("yaml does not exist", func(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
yamlPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-yaml-archived"))
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to get absolute YAML path").Error())
|
||||
}
|
||||
|
||||
// execute
|
||||
if err := migrateToV1(ctx); err != nil {
|
||||
t.Fatal(errors.Wrapf(err, "Failed to migrate").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
if utils.FileExists(yamlPath) {
|
||||
t.Fatal("YAML archive file must not exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrateToV2(t *testing.T) {
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.CopyFixture(ctx, "./fixtures/2-pre-dnote.json", "dnote")
|
||||
|
||||
// execute
|
||||
if err := migrateToV2(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
b := testutils.ReadFile(ctx, "dnote")
|
||||
|
||||
var postDnote migrateToV2PostDnote
|
||||
if err := json.Unmarshal(b, &postDnote); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
|
||||
}
|
||||
|
||||
for _, book := range postDnote {
|
||||
testutils.AssertNotEqual(t, book.Name, "", "Book name was not populated")
|
||||
|
||||
for _, note := range book.Notes {
|
||||
if len(note.UUID) == 8 {
|
||||
t.Errorf("Note UUID was not migrated. It has length of %d", len(note.UUID))
|
||||
}
|
||||
|
||||
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "AddedOn was not carried over")
|
||||
testutils.AssertEqual(t, note.EditedOn, int64(0), "EditedOn was not created properly")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateToV3(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.CopyFixture(ctx, "./fixtures/3-pre-dnote.json", "dnote")
|
||||
|
||||
// execute
|
||||
if err := migrateToV3(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
b := testutils.ReadFile(ctx, "dnote")
|
||||
var postDnote migrateToV3Dnote
|
||||
if err := json.Unmarshal(b, &postDnote); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
|
||||
}
|
||||
|
||||
b = testutils.ReadFile(ctx, "actions")
|
||||
var actions []migrateToV3Action
|
||||
if err := json.Unmarshal(b, &actions); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the actions").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, len(actions), 6, "actions length mismatch")
|
||||
|
||||
for _, book := range postDnote {
|
||||
for _, note := range book.Notes {
|
||||
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "AddedOn was not carried over")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateToV4(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
defer os.Setenv("EDITOR", "")
|
||||
|
||||
testutils.CopyFixture(ctx, "./fixtures/4-pre-dnoterc.yaml", "dnoterc")
|
||||
|
||||
// execute
|
||||
os.Setenv("EDITOR", "vim")
|
||||
if err := migrateToV4(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
b := testutils.ReadFile(ctx, "dnoterc")
|
||||
var config migrateToV4PostConfig
|
||||
if err := yaml.Unmarshal(b, &config); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, config.APIKey, "Oev6e1082ORasdf9rjkfjkasdfjhgei", "api key mismatch")
|
||||
testutils.AssertEqual(t, config.Editor, "vim", "editor mismatch")
|
||||
}
|
||||
|
||||
func TestMigrateToV5(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.CopyFixture(ctx, "./fixtures/5-pre-actions.json", "actions")
|
||||
|
||||
// execute
|
||||
if err := migrateToV5(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "migrating").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
var oldActions []migrateToV5PreAction
|
||||
testutils.ReadJSON("./fixtures/5-pre-actions.json", &oldActions)
|
||||
|
||||
b := testutils.ReadFile(ctx, "actions")
|
||||
var migratedActions []migrateToV5PostAction
|
||||
if err := json.Unmarshal(b, &migratedActions); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling migrated actions").Error())
|
||||
}
|
||||
|
||||
if len(oldActions) != len(migratedActions) {
|
||||
t.Fatalf("There were %d actions but after migration there were %d", len(oldActions), len(migratedActions))
|
||||
}
|
||||
|
||||
for idx := range migratedActions {
|
||||
migrated := migratedActions[idx]
|
||||
old := oldActions[idx]
|
||||
|
||||
testutils.AssertNotEqual(t, migrated.UUID, "", fmt.Sprintf("uuid mismatch for migrated item with index %d", idx))
|
||||
testutils.AssertEqual(t, migrated.Schema, 1, fmt.Sprintf("schema mismatch for migrated item with index %d", idx))
|
||||
testutils.AssertEqual(t, migrated.Timestamp, old.Timestamp, fmt.Sprintf("timestamp mismatch for migrated item with index %d", idx))
|
||||
testutils.AssertEqual(t, migrated.Type, old.Type, fmt.Sprintf("timestamp mismatch for migrated item with index %d", idx))
|
||||
|
||||
switch migrated.Type {
|
||||
case migrateToV5ActionAddNote:
|
||||
var oldData, migratedData migrateToV5AddNoteData
|
||||
if err := json.Unmarshal(old.Data, &oldData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling old data").Error())
|
||||
}
|
||||
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling new data").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
|
||||
testutils.AssertEqual(t, oldData.Content, migratedData.Content, fmt.Sprintf("data content mismatch for item idx %d", idx))
|
||||
testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx))
|
||||
case migrateToV5ActionRemoveNote:
|
||||
var oldData, migratedData migrateToV5RemoveNoteData
|
||||
if err := json.Unmarshal(old.Data, &oldData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling old data").Error())
|
||||
}
|
||||
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling new data").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
|
||||
testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx))
|
||||
case migrateToV5ActionAddBook:
|
||||
var oldData, migratedData migrateToV5AddBookData
|
||||
if err := json.Unmarshal(old.Data, &oldData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling old data").Error())
|
||||
}
|
||||
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling new data").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
|
||||
case migrateToV5ActionRemoveBook:
|
||||
var oldData, migratedData migrateToV5RemoveBookData
|
||||
if err := json.Unmarshal(old.Data, &oldData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling old data").Error())
|
||||
}
|
||||
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling new data").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
|
||||
case migrateToV5ActionEditNote:
|
||||
var oldData migrateToV5PreEditNoteData
|
||||
var migratedData migrateToV5PostEditNoteData
|
||||
if err := json.Unmarshal(old.Data, &oldData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling old data").Error())
|
||||
}
|
||||
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling new data").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx))
|
||||
testutils.AssertEqual(t, oldData.Content, migratedData.Content, fmt.Sprintf("data content mismatch for item idx %d", idx))
|
||||
testutils.AssertEqual(t, oldData.BookName, migratedData.FromBook, "book_name should have been renamed to from_book")
|
||||
testutils.AssertEqual(t, migratedData.ToBook, "", "to_book should be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateToV6(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.CopyFixture(ctx, "./fixtures/6-pre-dnote.json", "dnote")
|
||||
|
||||
// execute
|
||||
if err := migrateToV6(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
b := testutils.ReadFile(ctx, "dnote")
|
||||
var got migrateToV6PostDnote
|
||||
if err := json.Unmarshal(b, &got); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
|
||||
}
|
||||
|
||||
b = testutils.ReadFileAbs("./fixtures/6-post-dnote.json")
|
||||
var expected migrateToV6PostDnote
|
||||
if err := json.Unmarshal(b, &expected); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
|
||||
}
|
||||
|
||||
if ok := reflect.DeepEqual(expected, got); !ok {
|
||||
t.Errorf("Payload does not match.\nActual: %+v\nExpected: %+v", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateToV7(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
testutils.CopyFixture(ctx, "./fixtures/7-pre-actions.json", "actions")
|
||||
|
||||
// execute
|
||||
if err := migrateToV7(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "migrating").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
b := testutils.ReadFile(ctx, "actions")
|
||||
var got []migrateToV7Action
|
||||
if err := json.Unmarshal(b, &got); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling the result").Error())
|
||||
}
|
||||
|
||||
b2 := testutils.ReadFileAbs("./fixtures/7-post-actions.json")
|
||||
var expected []migrateToV7Action
|
||||
if err := json.Unmarshal(b, &expected); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling the result into Dnote").Error())
|
||||
}
|
||||
|
||||
ok, err := testutils.IsEqualJSON(b, b2)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "comparing JSON").Error())
|
||||
}
|
||||
|
||||
if !ok {
|
||||
t.Errorf("Result does not match.\nActual: %+v\nExpected: %+v", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateToV8(t *testing.T) {
|
||||
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
// set up
|
||||
testutils.CopyFixture(ctx, "./fixtures/8-actions.json", "actions")
|
||||
testutils.CopyFixture(ctx, "./fixtures/8-dnote.json", "dnote")
|
||||
testutils.CopyFixture(ctx, "./fixtures/8-dnoterc.yaml", "dnoterc")
|
||||
testutils.CopyFixture(ctx, "./fixtures/8-schema.yaml", "schema")
|
||||
testutils.CopyFixture(ctx, "./fixtures/8-timestamps.yaml", "timestamps")
|
||||
|
||||
// execute
|
||||
if err := migrateToV8(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "migrating").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
|
||||
// 1. test if files are migrated
|
||||
dnoteFilePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
|
||||
dnotercPath := fmt.Sprintf("%s/dnoterc", ctx.DnoteDir)
|
||||
schemaFilePath := fmt.Sprintf("%s/schema", ctx.DnoteDir)
|
||||
timestampFilePath := fmt.Sprintf("%s/timestamps", ctx.DnoteDir)
|
||||
if ok := utils.FileExists(dnoteFilePath); ok {
|
||||
t.Errorf("%s still exists", dnoteFilePath)
|
||||
}
|
||||
if ok := utils.FileExists(schemaFilePath); ok {
|
||||
t.Errorf("%s still exists", dnoteFilePath)
|
||||
}
|
||||
if ok := utils.FileExists(timestampFilePath); ok {
|
||||
t.Errorf("%s still exists", dnoteFilePath)
|
||||
}
|
||||
if ok := utils.FileExists(dnotercPath); !ok {
|
||||
t.Errorf("%s should exist", dnotercPath)
|
||||
}
|
||||
|
||||
// 2. test if notes and books are migrated
|
||||
db := ctx.DB
|
||||
|
||||
var bookCount, noteCount int
|
||||
err := db.QueryRow("SELECT count(*) FROM books").Scan(&bookCount)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "counting books"))
|
||||
}
|
||||
err = db.QueryRow("SELECT count(*) FROM notes").Scan(¬eCount)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "counting notes"))
|
||||
}
|
||||
testutils.AssertEqual(t, bookCount, 2, "book count mismatch")
|
||||
testutils.AssertEqual(t, noteCount, 3, "note count mismatch")
|
||||
|
||||
type bookInfo struct {
|
||||
label string
|
||||
uuid string
|
||||
}
|
||||
type noteInfo struct {
|
||||
id int
|
||||
uuid string
|
||||
bookUUID string
|
||||
content string
|
||||
addedOn int64
|
||||
editedOn int64
|
||||
public bool
|
||||
}
|
||||
|
||||
var b1, b2 bookInfo
|
||||
var n1, n2, n3 noteInfo
|
||||
err = db.QueryRow("SELECT label, uuid FROM books WHERE label = ?", "js").Scan(&b1.label, &b1.uuid)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding book 1"))
|
||||
}
|
||||
err = db.QueryRow("SELECT label, uuid FROM books WHERE label = ?", "css").Scan(&b2.label, &b2.uuid)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding book 2"))
|
||||
}
|
||||
err = db.QueryRow("SELECT id, uuid, book_uuid, content, added_on, edited_on, public FROM notes WHERE uuid = ?", "d69edb54-5b31-4cdd-a4a5-34f0a0bfa153").Scan(&n1.id, &n1.uuid, &n1.bookUUID, &n1.content, &n1.addedOn, &n1.editedOn, &n1.public)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding note 1"))
|
||||
}
|
||||
err = db.QueryRow("SELECT id, uuid, book_uuid, content, added_on, edited_on, public FROM notes WHERE uuid = ?", "35cbcab1-6a2a-4cc8-97e0-e73bbbd54626").Scan(&n2.id, &n2.uuid, &n2.bookUUID, &n2.content, &n2.addedOn, &n2.editedOn, &n2.public)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding note 2"))
|
||||
}
|
||||
err = db.QueryRow("SELECT id, uuid, book_uuid, content, added_on, edited_on, public FROM notes WHERE uuid = ?", "7c1fcfb2-de8b-4350-88f0-fb3cbaf6630a").Scan(&n3.id, &n3.uuid, &n3.bookUUID, &n3.content, &n3.addedOn, &n3.editedOn, &n3.public)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding note 3"))
|
||||
}
|
||||
|
||||
testutils.AssertNotEqual(t, b1.uuid, "", "book 1 uuid should have been generated")
|
||||
testutils.AssertEqual(t, b1.label, "js", "book 1 label mismatch")
|
||||
testutils.AssertNotEqual(t, b2.uuid, "", "book 2 uuid should have been generated")
|
||||
testutils.AssertEqual(t, b2.label, "css", "book 2 label mismatch")
|
||||
|
||||
testutils.AssertEqual(t, n1.uuid, "d69edb54-5b31-4cdd-a4a5-34f0a0bfa153", "note 1 uuid mismatch")
|
||||
testutils.AssertNotEqual(t, n1.id, 0, "note 1 id should have been generated")
|
||||
testutils.AssertEqual(t, n1.bookUUID, b2.uuid, "note 1 book_uuid mismatch")
|
||||
testutils.AssertEqual(t, n1.content, "css test 1", "note 1 content mismatch")
|
||||
testutils.AssertEqual(t, n1.addedOn, int64(1536977237), "note 1 added_on mismatch")
|
||||
testutils.AssertEqual(t, n1.editedOn, int64(1536977253), "note 1 edited_on mismatch")
|
||||
testutils.AssertEqual(t, n1.public, false, "note 1 public mismatch")
|
||||
|
||||
testutils.AssertEqual(t, n2.uuid, "35cbcab1-6a2a-4cc8-97e0-e73bbbd54626", "note 2 uuid mismatch")
|
||||
testutils.AssertNotEqual(t, n2.id, 0, "note 2 id should have been generated")
|
||||
testutils.AssertEqual(t, n2.bookUUID, b1.uuid, "note 2 book_uuid mismatch")
|
||||
testutils.AssertEqual(t, n2.content, "js test 1", "note 2 content mismatch")
|
||||
testutils.AssertEqual(t, n2.addedOn, int64(1536977229), "note 2 added_on mismatch")
|
||||
testutils.AssertEqual(t, n2.editedOn, int64(0), "note 2 edited_on mismatch")
|
||||
testutils.AssertEqual(t, n2.public, false, "note 2 public mismatch")
|
||||
|
||||
testutils.AssertEqual(t, n3.uuid, "7c1fcfb2-de8b-4350-88f0-fb3cbaf6630a", "note 3 uuid mismatch")
|
||||
testutils.AssertNotEqual(t, n3.id, 0, "note 3 id should have been generated")
|
||||
testutils.AssertEqual(t, n3.bookUUID, b1.uuid, "note 3 book_uuid mismatch")
|
||||
testutils.AssertEqual(t, n3.content, "js test 2", "note 3 content mismatch")
|
||||
testutils.AssertEqual(t, n3.addedOn, int64(1536977230), "note 3 added_on mismatch")
|
||||
testutils.AssertEqual(t, n3.editedOn, int64(0), "note 3 edited_on mismatch")
|
||||
testutils.AssertEqual(t, n3.public, false, "note 3 public mismatch")
|
||||
|
||||
// 3. test if actions are migrated
|
||||
var actionCount int
|
||||
err = db.QueryRow("SELECT count(*) FROM actions").Scan(&actionCount)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "counting actions"))
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, actionCount, 11, "action count mismatch")
|
||||
|
||||
type actionInfo struct {
|
||||
uuid string
|
||||
schema int
|
||||
actionType string
|
||||
data string
|
||||
timestamp int
|
||||
}
|
||||
|
||||
var a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11 actionInfo
|
||||
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "6145c1b7-f286-4d9f-b0f6-00d274baefc6").Scan(&a1.uuid, &a1.schema, &a1.actionType, &a1.data, &a1.timestamp)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding a1"))
|
||||
}
|
||||
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "c048a56b-179c-4f31-9995-81e9b32b7dd6").Scan(&a2.uuid, &a2.schema, &a2.actionType, &a2.data, &a2.timestamp)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding a2"))
|
||||
}
|
||||
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "f557ef48-c304-47dc-adfb-46b7306e701f").Scan(&a3.uuid, &a3.schema, &a3.actionType, &a3.data, &a3.timestamp)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding a3"))
|
||||
}
|
||||
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "8d79db34-343d-4331-ae5b-24743f17ca7f").Scan(&a4.uuid, &a4.schema, &a4.actionType, &a4.data, &a4.timestamp)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding a4"))
|
||||
}
|
||||
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "b9c1ed4a-e6b3-41f2-983b-593ec7b8b7a1").Scan(&a5.uuid, &a5.schema, &a5.actionType, &a5.data, &a5.timestamp)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding a5"))
|
||||
}
|
||||
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "06ed7ef0-f171-4bd7-ae8e-97b5d06a4c49").Scan(&a6.uuid, &a6.schema, &a6.actionType, &a6.data, &a6.timestamp)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding a6"))
|
||||
}
|
||||
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "7f173cef-1688-4177-a373-145fcd822b2f").Scan(&a7.uuid, &a7.schema, &a7.actionType, &a7.data, &a7.timestamp)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding a7"))
|
||||
}
|
||||
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "64352e08-aa7a-45f4-b760-b3f38b5e11fa").Scan(&a8.uuid, &a8.schema, &a8.actionType, &a8.data, &a8.timestamp)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding a8"))
|
||||
}
|
||||
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "82e20a12-bda8-45f7-ac42-b453b6daa5ec").Scan(&a9.uuid, &a9.schema, &a9.actionType, &a9.data, &a9.timestamp)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding a9"))
|
||||
}
|
||||
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "a29055f4-ace4-44fd-8800-3396edbccaef").Scan(&a10.uuid, &a10.schema, &a10.actionType, &a10.data, &a10.timestamp)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding a10"))
|
||||
}
|
||||
err = db.QueryRow("SELECT uuid, schema, type, data, timestamp FROM actions WHERE uuid = ?", "871a5562-1bd0-43c1-b550-5bbb727ac7c4").Scan(&a11.uuid, &a11.schema, &a11.actionType, &a11.data, &a11.timestamp)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding a11"))
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, a1.uuid, "6145c1b7-f286-4d9f-b0f6-00d274baefc6", "action 1 uuid mismatch")
|
||||
testutils.AssertEqual(t, a1.schema, 1, "action 1 schema mismatch")
|
||||
testutils.AssertEqual(t, a1.actionType, "add_book", "action 1 type mismatch")
|
||||
testutils.AssertEqual(t, a1.data, `{"book_name":"js"}`, "action 1 data mismatch")
|
||||
testutils.AssertEqual(t, a1.timestamp, 1536977229, "action 1 timestamp mismatch")
|
||||
|
||||
testutils.AssertEqual(t, a2.uuid, "c048a56b-179c-4f31-9995-81e9b32b7dd6", "action 2 uuid mismatch")
|
||||
testutils.AssertEqual(t, a2.schema, 2, "action 2 schema mismatch")
|
||||
testutils.AssertEqual(t, a2.actionType, "add_note", "action 2 type mismatch")
|
||||
testutils.AssertEqual(t, a2.data, `{"note_uuid":"35cbcab1-6a2a-4cc8-97e0-e73bbbd54626","book_name":"js","content":"js test 1","public":false}`, "action 2 data mismatch")
|
||||
testutils.AssertEqual(t, a2.timestamp, 1536977229, "action 2 timestamp mismatch")
|
||||
|
||||
testutils.AssertEqual(t, a3.uuid, "f557ef48-c304-47dc-adfb-46b7306e701f", "action 3 uuid mismatch")
|
||||
testutils.AssertEqual(t, a3.schema, 2, "action 3 schema mismatch")
|
||||
testutils.AssertEqual(t, a3.actionType, "add_note", "action 3 type mismatch")
|
||||
testutils.AssertEqual(t, a3.data, `{"note_uuid":"7c1fcfb2-de8b-4350-88f0-fb3cbaf6630a","book_name":"js","content":"js test 2","public":false}`, "action 3 data mismatch")
|
||||
testutils.AssertEqual(t, a3.timestamp, 1536977230, "action 3 timestamp mismatch")
|
||||
|
||||
testutils.AssertEqual(t, a4.uuid, "8d79db34-343d-4331-ae5b-24743f17ca7f", "action 4 uuid mismatch")
|
||||
testutils.AssertEqual(t, a4.schema, 2, "action 4 schema mismatch")
|
||||
testutils.AssertEqual(t, a4.actionType, "add_note", "action 4 type mismatch")
|
||||
testutils.AssertEqual(t, a4.data, `{"note_uuid":"b23a88ba-b291-4294-9795-86b394db5dcf","book_name":"js","content":"js test 3","public":false}`, "action 4 data mismatch")
|
||||
testutils.AssertEqual(t, a4.timestamp, 1536977234, "action 4 timestamp mismatch")
|
||||
|
||||
testutils.AssertEqual(t, a5.uuid, "b9c1ed4a-e6b3-41f2-983b-593ec7b8b7a1", "action 5 uuid mismatch")
|
||||
testutils.AssertEqual(t, a5.schema, 1, "action 5 schema mismatch")
|
||||
testutils.AssertEqual(t, a5.actionType, "add_book", "action 5 type mismatch")
|
||||
testutils.AssertEqual(t, a5.data, `{"book_name":"css"}`, "action 5 data mismatch")
|
||||
testutils.AssertEqual(t, a5.timestamp, 1536977237, "action 5 timestamp mismatch")
|
||||
|
||||
testutils.AssertEqual(t, a6.uuid, "06ed7ef0-f171-4bd7-ae8e-97b5d06a4c49", "action 6 uuid mismatch")
|
||||
testutils.AssertEqual(t, a6.schema, 2, "action 6 schema mismatch")
|
||||
testutils.AssertEqual(t, a6.actionType, "add_note", "action 6 type mismatch")
|
||||
testutils.AssertEqual(t, a6.data, `{"note_uuid":"d69edb54-5b31-4cdd-a4a5-34f0a0bfa153","book_name":"css","content":"js test 3","public":false}`, "action 6 data mismatch")
|
||||
testutils.AssertEqual(t, a6.timestamp, 1536977237, "action 6 timestamp mismatch")
|
||||
|
||||
testutils.AssertEqual(t, a7.uuid, "7f173cef-1688-4177-a373-145fcd822b2f", "action 7 uuid mismatch")
|
||||
testutils.AssertEqual(t, a7.schema, 2, "action 7 schema mismatch")
|
||||
testutils.AssertEqual(t, a7.actionType, "edit_note", "action 7 type mismatch")
|
||||
testutils.AssertEqual(t, a7.data, `{"note_uuid":"d69edb54-5b31-4cdd-a4a5-34f0a0bfa153","from_book":"css","to_book":null,"content":"css test 1","public":null}`, "action 7 data mismatch")
|
||||
testutils.AssertEqual(t, a7.timestamp, 1536977253, "action 7 timestamp mismatch")
|
||||
|
||||
testutils.AssertEqual(t, a8.uuid, "64352e08-aa7a-45f4-b760-b3f38b5e11fa", "action 8 uuid mismatch")
|
||||
testutils.AssertEqual(t, a8.schema, 1, "action 8 schema mismatch")
|
||||
testutils.AssertEqual(t, a8.actionType, "add_book", "action 8 type mismatch")
|
||||
testutils.AssertEqual(t, a8.data, `{"book_name":"sql"}`, "action 8 data mismatch")
|
||||
testutils.AssertEqual(t, a8.timestamp, 1536977261, "action 8 timestamp mismatch")
|
||||
|
||||
testutils.AssertEqual(t, a9.uuid, "82e20a12-bda8-45f7-ac42-b453b6daa5ec", "action 9 uuid mismatch")
|
||||
testutils.AssertEqual(t, a9.schema, 2, "action 9 schema mismatch")
|
||||
testutils.AssertEqual(t, a9.actionType, "add_note", "action 9 type mismatch")
|
||||
testutils.AssertEqual(t, a9.data, `{"note_uuid":"2f47d390-685b-4b84-89ac-704c6fb8d3fb","book_name":"sql","content":"blah","public":false}`, "action 9 data mismatch")
|
||||
testutils.AssertEqual(t, a9.timestamp, 1536977261, "action 9 timestamp mismatch")
|
||||
|
||||
testutils.AssertEqual(t, a10.uuid, "a29055f4-ace4-44fd-8800-3396edbccaef", "action 10 uuid mismatch")
|
||||
testutils.AssertEqual(t, a10.schema, 1, "action 10 schema mismatch")
|
||||
testutils.AssertEqual(t, a10.actionType, "remove_book", "action 10 type mismatch")
|
||||
testutils.AssertEqual(t, a10.data, `{"book_name":"sql"}`, "action 10 data mismatch")
|
||||
testutils.AssertEqual(t, a10.timestamp, 1536977268, "action 10 timestamp mismatch")
|
||||
|
||||
testutils.AssertEqual(t, a11.uuid, "871a5562-1bd0-43c1-b550-5bbb727ac7c4", "action 11 uuid mismatch")
|
||||
testutils.AssertEqual(t, a11.schema, 1, "action 11 schema mismatch")
|
||||
testutils.AssertEqual(t, a11.actionType, "remove_note", "action 11 type mismatch")
|
||||
testutils.AssertEqual(t, a11.data, `{"note_uuid":"b23a88ba-b291-4294-9795-86b394db5dcf","book_name":"js"}`, "action 11 data mismatch")
|
||||
testutils.AssertEqual(t, a11.timestamp, 1536977274, "action 11 timestamp mismatch")
|
||||
|
||||
// 3. test if system is migrated
|
||||
var systemCount int
|
||||
err = db.QueryRow("SELECT count(*) FROM system").Scan(&systemCount)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "counting system"))
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, systemCount, 3, "action count mismatch")
|
||||
|
||||
var lastUpgrade, lastAction, bookmark int
|
||||
err = db.QueryRow("SELECT value FROM system WHERE key = ?", "last_upgrade").Scan(&lastUpgrade)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding last_upgrade"))
|
||||
}
|
||||
err = db.QueryRow("SELECT value FROM system WHERE key = ?", "last_action").Scan(&lastAction)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding last_action"))
|
||||
}
|
||||
err = db.QueryRow("SELECT value FROM system WHERE key = ?", "bookmark").Scan(&bookmark)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "finding bookmark"))
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, lastUpgrade, 1536977220, "last_upgrade mismatch")
|
||||
testutils.AssertEqual(t, lastAction, 1536977274, "last_action mismatch")
|
||||
testutils.AssertEqual(t, bookmark, 9, "bookmark mismatch")
|
||||
}
|
||||
|
|
@ -1,253 +1,95 @@
|
|||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"database/sql"
|
||||
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/utils"
|
||||
"github.com/dnote/cli/log"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
schemaFilename = "schema"
|
||||
backupDirName = ".dnote-bak"
|
||||
)
|
||||
|
||||
// migration IDs
|
||||
const (
|
||||
_ = iota
|
||||
migrationV1
|
||||
migrationV2
|
||||
migrationV3
|
||||
migrationV4
|
||||
migrationV5
|
||||
migrationV6
|
||||
migrationV7
|
||||
)
|
||||
|
||||
var migrationSequence = []int{
|
||||
migrationV1,
|
||||
migrationV2,
|
||||
migrationV3,
|
||||
migrationV4,
|
||||
migrationV5,
|
||||
migrationV6,
|
||||
migrationV7,
|
||||
type migration struct {
|
||||
name string
|
||||
sql string
|
||||
}
|
||||
|
||||
type schema struct {
|
||||
CurrentVersion int `yaml:"current_version"`
|
||||
}
|
||||
var migrations = []migration{}
|
||||
|
||||
func makeSchema(complete bool) schema {
|
||||
s := schema{}
|
||||
func initSchema(db *sql.DB) (int, error) {
|
||||
schemaVersion := 0
|
||||
|
||||
var currentVersion int
|
||||
if complete {
|
||||
currentVersion = len(migrationSequence)
|
||||
}
|
||||
|
||||
s.CurrentVersion = currentVersion
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Migrate determines migrations to be run and performs them
|
||||
func Migrate(ctx infra.DnoteCtx) error {
|
||||
unrunMigrations, err := getUnrunMigrations(ctx)
|
||||
_, err := db.Exec("INSERT INTO system (key, value) VALUES (? ,?)", "schema", schemaVersion)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get unrun migrations")
|
||||
return schemaVersion, errors.Wrap(err, "inserting schema")
|
||||
}
|
||||
|
||||
for _, mid := range unrunMigrations {
|
||||
if err := performMigration(ctx, mid); err != nil {
|
||||
return errors.Wrapf(err, "Failed to run migration #%d", mid)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return schemaVersion, nil
|
||||
}
|
||||
|
||||
// performMigration backs up current .dnote data, performs migration, and
|
||||
// restores or cleans backups depending on if there is an error
|
||||
func performMigration(ctx infra.DnoteCtx, migrationID int) error {
|
||||
if err := backupDnoteDir(ctx); err != nil {
|
||||
return errors.Wrap(err, "Failed to back up dnote directory")
|
||||
}
|
||||
func getSchema(db *sql.DB) (int, error) {
|
||||
var ret int
|
||||
|
||||
var migrationError error
|
||||
err := db.QueryRow("SELECT value FROM system where key = ?", "schema").Scan(&ret)
|
||||
if err == sql.ErrNoRows {
|
||||
ret, err = initSchema(db)
|
||||
|
||||
switch migrationID {
|
||||
case migrationV1:
|
||||
migrationError = migrateToV1(ctx)
|
||||
case migrationV2:
|
||||
migrationError = migrateToV2(ctx)
|
||||
case migrationV3:
|
||||
migrationError = migrateToV3(ctx)
|
||||
case migrationV4:
|
||||
migrationError = migrateToV4(ctx)
|
||||
case migrationV5:
|
||||
migrationError = migrateToV5(ctx)
|
||||
case migrationV6:
|
||||
migrationError = migrateToV6(ctx)
|
||||
case migrationV7:
|
||||
migrationError = migrateToV7(ctx)
|
||||
default:
|
||||
return errors.Errorf("Unrecognized migration id %d", migrationID)
|
||||
}
|
||||
|
||||
if migrationError != nil {
|
||||
if err := restoreBackup(ctx); err != nil {
|
||||
panic(errors.Wrap(err, "Failed to restore backup for a failed migration"))
|
||||
}
|
||||
|
||||
return errors.Wrapf(migrationError, "Failed to perform migration #%d", migrationID)
|
||||
}
|
||||
|
||||
if err := clearBackup(ctx); err != nil {
|
||||
return errors.Wrap(err, "Failed to clear backup")
|
||||
}
|
||||
|
||||
if err := updateSchemaVersion(ctx, migrationID); err != nil {
|
||||
return errors.Wrap(err, "Failed to update schema version")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// backupDnoteDir backs up the dnote directory to a temporary backup directory
|
||||
func backupDnoteDir(ctx infra.DnoteCtx) error {
|
||||
srcPath := fmt.Sprintf("%s/.dnote", ctx.HomeDir)
|
||||
tmpPath := fmt.Sprintf("%s/%s", ctx.HomeDir, backupDirName)
|
||||
|
||||
if err := utils.CopyDir(srcPath, tmpPath); err != nil {
|
||||
return errors.Wrap(err, "Failed to copy the .dnote directory")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreBackup(ctx infra.DnoteCtx) error {
|
||||
var err error
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.Printf(`Failed to restore backup for a failed migration.
|
||||
Don't worry. Your data is still intact in the backup directory.
|
||||
Get help on https://github.com/dnote/cli/issues`)
|
||||
return ret, errors.Wrap(err, "initializing schema")
|
||||
}
|
||||
}()
|
||||
|
||||
srcPath := fmt.Sprintf("%s/.dnote", ctx.HomeDir)
|
||||
backupPath := fmt.Sprintf("%s/%s", ctx.HomeDir, backupDirName)
|
||||
|
||||
if err = os.RemoveAll(srcPath); err != nil {
|
||||
return errors.Wrapf(err, "Failed to clear current dnote data at %s", backupPath)
|
||||
} else if err != nil {
|
||||
return ret, errors.Wrap(err, "querying schema")
|
||||
}
|
||||
|
||||
if err = os.Rename(backupPath, srcPath); err != nil {
|
||||
return errors.Wrap(err, `Failed to copy backup data to the original directory.`)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func execute(ctx infra.DnoteCtx, nextSchema int, m migration) error {
|
||||
log.Debug("running migration %s\n", m.name)
|
||||
|
||||
tx, err := ctx.DB.Begin()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "beginning a transaction")
|
||||
}
|
||||
|
||||
_, err = tx.Exec(m.sql)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "running sql")
|
||||
}
|
||||
|
||||
_, err = tx.Exec("UPDATE system SET value = ? WHERE key = ?", nextSchema, "schema")
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return errors.Wrap(err, "incrementing schema")
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func clearBackup(ctx infra.DnoteCtx) error {
|
||||
backupPath := fmt.Sprintf("%s/%s", ctx.HomeDir, backupDirName)
|
||||
// Run performs unrun migrations
|
||||
func Run(ctx infra.DnoteCtx) error {
|
||||
db := ctx.DB
|
||||
|
||||
if err := os.RemoveAll(backupPath); err != nil {
|
||||
return errors.Wrapf(err, "Failed to remove backup at %s", backupPath)
|
||||
schema, err := getSchema(db)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting the current schema")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
log.Debug("current schema %d\n", schema)
|
||||
|
||||
// getSchemaPath returns the path to the file containing schema info
|
||||
func getSchemaPath(ctx infra.DnoteCtx) string {
|
||||
return fmt.Sprintf("%s/%s", ctx.DnoteDir, schemaFilename)
|
||||
}
|
||||
|
||||
// InitSchemaFile creates a migration file
|
||||
func InitSchemaFile(ctx infra.DnoteCtx, pristine bool) error {
|
||||
path := getSchemaPath(ctx)
|
||||
if utils.FileExists(path) {
|
||||
if schema == len(migrations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := makeSchema(pristine)
|
||||
err := writeSchema(ctx, s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write schema")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSchema(ctx infra.DnoteCtx) (schema, error) {
|
||||
var ret schema
|
||||
|
||||
path := getSchemaPath(ctx)
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "Failed to read schema file")
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(b, &ret)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "Failed to unmarshal the schema JSON")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func writeSchema(ctx infra.DnoteCtx, s schema) error {
|
||||
path := getSchemaPath(ctx)
|
||||
d, err := yaml.Marshal(&s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal schema into yaml")
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, d, 0644); err != nil {
|
||||
return errors.Wrap(err, "Failed to write schema file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUnrunMigrations(ctx infra.DnoteCtx) ([]int, error) {
|
||||
var ret []int
|
||||
|
||||
schema, err := readSchema(ctx)
|
||||
if err != nil {
|
||||
return ret, errors.Wrap(err, "Failed to read schema")
|
||||
}
|
||||
|
||||
if schema.CurrentVersion == len(migrationSequence) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
nextVersion := schema.CurrentVersion
|
||||
ret = migrationSequence[nextVersion:]
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func updateSchemaVersion(ctx infra.DnoteCtx, mID int) error {
|
||||
s, err := readSchema(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read schema")
|
||||
}
|
||||
|
||||
s.CurrentVersion = mID
|
||||
|
||||
err = writeSchema(ctx, s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write schema")
|
||||
toRun := migrations[schema:]
|
||||
|
||||
for idx, m := range toRun {
|
||||
nextSchema := schema + idx + 1
|
||||
if err := execute(ctx, nextSchema, m); err != nil {
|
||||
return errors.Wrapf(err, "running migration %s", m.name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,362 +1,9 @@
|
|||
package migrate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/cli/testutils"
|
||||
"github.com/dnote/cli/utils"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestMigrateAll(t *testing.T) {
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
func TestExecute(t *testing.T) {
|
||||
|
||||
// set up
|
||||
testutils.SetupTmp(ctx)
|
||||
testutils.WriteFile(ctx, "./fixtures/2-pre-dnote.json", "dnote")
|
||||
testutils.WriteFile(ctx, "./fixtures/4-pre-dnoterc.yaml", "dnoterc")
|
||||
if err := InitSchemaFile(ctx, false); err != nil {
|
||||
panic(errors.Wrap(err, "Failed to initialize schema file"))
|
||||
}
|
||||
defer testutils.ClearTmp(ctx)
|
||||
|
||||
// Execute
|
||||
if err := Migrate(ctx); err != nil {
|
||||
t.Fatalf("Failed to migrate %s", err.Error())
|
||||
}
|
||||
|
||||
// Test
|
||||
schema, err := readSchema(ctx)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to read the schema"))
|
||||
}
|
||||
|
||||
b := testutils.ReadFile(ctx, "dnote")
|
||||
var dnote migrateToV3Dnote
|
||||
if err := json.Unmarshal(b, &dnote); err != nil {
|
||||
t.Error(errors.Wrap(err, "Failed to unmarshal result into dnote").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, schema.CurrentVersion, len(migrationSequence), "current schema version mismatch")
|
||||
|
||||
note := dnote["algorithm"].Notes[0]
|
||||
testutils.AssertEqual(t, note.Content, "in-place means no extra space required. it mutates the input", "content was not carried over")
|
||||
testutils.AssertNotEqual(t, note.UUID, "", "note uuid was not generated")
|
||||
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "note added_on was not generated")
|
||||
testutils.AssertEqual(t, note.EditedOn, int64(0), "note edited_on was not propertly generated")
|
||||
}
|
||||
|
||||
func TestMigrateToV1(t *testing.T) {
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
t.Run("yaml exists", func(t *testing.T) {
|
||||
// set up
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
|
||||
yamlPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-yaml-archived"))
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to get absolute YAML path").Error())
|
||||
}
|
||||
ioutil.WriteFile(yamlPath, []byte{}, 0644)
|
||||
|
||||
// execute
|
||||
if err := migrateToV1(ctx); err != nil {
|
||||
t.Fatal(errors.Wrapf(err, "Failed to migrate").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
if utils.FileExists(yamlPath) {
|
||||
t.Fatal("YAML archive file has not been deleted")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("yaml does not exist", func(t *testing.T) {
|
||||
// set up
|
||||
testutils.SetupTmp(ctx)
|
||||
defer testutils.ClearTmp(ctx)
|
||||
|
||||
yamlPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-yaml-archived"))
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Failed to get absolute YAML path").Error())
|
||||
}
|
||||
|
||||
// execute
|
||||
if err := migrateToV1(ctx); err != nil {
|
||||
t.Fatal(errors.Wrapf(err, "Failed to migrate").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
if utils.FileExists(yamlPath) {
|
||||
t.Fatal("YAML archive file must not exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrateToV2(t *testing.T) {
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
// set up
|
||||
testutils.SetupTmp(ctx)
|
||||
testutils.WriteFile(ctx, "./fixtures/2-pre-dnote.json", "dnote")
|
||||
defer testutils.ClearTmp(ctx)
|
||||
|
||||
// execute
|
||||
if err := migrateToV2(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
b := testutils.ReadFile(ctx, "dnote")
|
||||
|
||||
var postDnote migrateToV2PostDnote
|
||||
if err := json.Unmarshal(b, &postDnote); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
|
||||
}
|
||||
|
||||
for _, book := range postDnote {
|
||||
testutils.AssertNotEqual(t, book.Name, "", "Book name was not populated")
|
||||
|
||||
for _, note := range book.Notes {
|
||||
if len(note.UUID) == 8 {
|
||||
t.Errorf("Note UUID was not migrated. It has length of %d", len(note.UUID))
|
||||
}
|
||||
|
||||
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "AddedOn was not carried over")
|
||||
testutils.AssertEqual(t, note.EditedOn, int64(0), "EditedOn was not created properly")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateToV3(t *testing.T) {
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
// set up
|
||||
testutils.SetupTmp(ctx)
|
||||
testutils.WriteFile(ctx, "./fixtures/3-pre-dnote.json", "dnote")
|
||||
defer testutils.ClearTmp(ctx)
|
||||
|
||||
// execute
|
||||
if err := migrateToV3(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
b := testutils.ReadFile(ctx, "dnote")
|
||||
var postDnote migrateToV3Dnote
|
||||
if err := json.Unmarshal(b, &postDnote); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
|
||||
}
|
||||
|
||||
b = testutils.ReadFile(ctx, "actions")
|
||||
var actions []migrateToV3Action
|
||||
if err := json.Unmarshal(b, &actions); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the actions").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, len(actions), 6, "actions length mismatch")
|
||||
|
||||
for _, book := range postDnote {
|
||||
for _, note := range book.Notes {
|
||||
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "AddedOn was not carried over")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateToV4(t *testing.T) {
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
// set up
|
||||
testutils.SetupTmp(ctx)
|
||||
testutils.WriteFile(ctx, "./fixtures/4-pre-dnoterc.yaml", "dnoterc")
|
||||
defer testutils.ClearTmp(ctx)
|
||||
defer os.Setenv("EDITOR", "")
|
||||
|
||||
// execute
|
||||
os.Setenv("EDITOR", "vim")
|
||||
if err := migrateToV4(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
b := testutils.ReadFile(ctx, "dnoterc")
|
||||
var config migrateToV4PostConfig
|
||||
if err := yaml.Unmarshal(b, &config); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, config.APIKey, "Oev6e1082ORasdf9rjkfjkasdfjhgei", "api key mismatch")
|
||||
testutils.AssertEqual(t, config.Editor, "vim", "editor mismatch")
|
||||
}
|
||||
|
||||
func TestMigrateToV5(t *testing.T) {
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
// set up
|
||||
testutils.SetupTmp(ctx)
|
||||
testutils.WriteFile(ctx, "./fixtures/5-pre-actions.json", "actions")
|
||||
defer testutils.ClearTmp(ctx)
|
||||
|
||||
// execute
|
||||
if err := migrateToV5(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "migrating").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
var oldActions []migrateToV5PreAction
|
||||
testutils.ReadJSON("./fixtures/5-pre-actions.json", &oldActions)
|
||||
|
||||
b := testutils.ReadFile(ctx, "actions")
|
||||
var migratedActions []migrateToV5PostAction
|
||||
if err := json.Unmarshal(b, &migratedActions); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarhsalling migrated actions").Error())
|
||||
}
|
||||
|
||||
if len(oldActions) != len(migratedActions) {
|
||||
t.Fatalf("There were %d actions but after migration there were %d", len(oldActions), len(migratedActions))
|
||||
}
|
||||
|
||||
for idx := range migratedActions {
|
||||
migrated := migratedActions[idx]
|
||||
old := oldActions[idx]
|
||||
|
||||
testutils.AssertNotEqual(t, migrated.UUID, "", fmt.Sprintf("uuid mismatch for migrated item with index %d", idx))
|
||||
testutils.AssertEqual(t, migrated.Schema, 1, fmt.Sprintf("schema mismatch for migrated item with index %d", idx))
|
||||
testutils.AssertEqual(t, migrated.Timestamp, old.Timestamp, fmt.Sprintf("timestamp mismatch for migrated item with index %d", idx))
|
||||
testutils.AssertEqual(t, migrated.Type, old.Type, fmt.Sprintf("timestamp mismatch for migrated item with index %d", idx))
|
||||
|
||||
switch migrated.Type {
|
||||
case migrateToV5ActionAddNote:
|
||||
var oldData, migratedData migrateToV5AddNoteData
|
||||
if err := json.Unmarshal(old.Data, &oldData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error())
|
||||
}
|
||||
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
|
||||
testutils.AssertEqual(t, oldData.Content, migratedData.Content, fmt.Sprintf("data content mismatch for item idx %d", idx))
|
||||
testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx))
|
||||
case migrateToV5ActionRemoveNote:
|
||||
var oldData, migratedData migrateToV5RemoveNoteData
|
||||
if err := json.Unmarshal(old.Data, &oldData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error())
|
||||
}
|
||||
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
|
||||
testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx))
|
||||
case migrateToV5ActionAddBook:
|
||||
var oldData, migratedData migrateToV5AddBookData
|
||||
if err := json.Unmarshal(old.Data, &oldData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error())
|
||||
}
|
||||
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
|
||||
case migrateToV5ActionRemoveBook:
|
||||
var oldData, migratedData migrateToV5RemoveBookData
|
||||
if err := json.Unmarshal(old.Data, &oldData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error())
|
||||
}
|
||||
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx))
|
||||
case migrateToV5ActionEditNote:
|
||||
var oldData migrateToV5PreEditNoteData
|
||||
var migratedData migrateToV5PostEditNoteData
|
||||
if err := json.Unmarshal(old.Data, &oldData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error())
|
||||
}
|
||||
if err := json.Unmarshal(migrated.Data, &migratedData); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error())
|
||||
}
|
||||
|
||||
testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx))
|
||||
testutils.AssertEqual(t, oldData.Content, migratedData.Content, fmt.Sprintf("data content mismatch for item idx %d", idx))
|
||||
testutils.AssertEqual(t, oldData.BookName, migratedData.FromBook, "book_name should have been renamed to from_book")
|
||||
testutils.AssertEqual(t, migratedData.ToBook, "", "to_book should be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateToV6(t *testing.T) {
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
// set up
|
||||
testutils.SetupTmp(ctx)
|
||||
testutils.WriteFile(ctx, "./fixtures/6-pre-dnote.json", "dnote")
|
||||
defer testutils.ClearTmp(ctx)
|
||||
|
||||
// execute
|
||||
if err := migrateToV6(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to migrate").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
b := testutils.ReadFile(ctx, "dnote")
|
||||
var got migrateToV6PostDnote
|
||||
if err := json.Unmarshal(b, &got); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
|
||||
}
|
||||
|
||||
b = testutils.ReadFileAbs("./fixtures/6-post-dnote.json")
|
||||
var expected migrateToV6PostDnote
|
||||
if err := json.Unmarshal(b, &expected); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
|
||||
}
|
||||
|
||||
if ok := reflect.DeepEqual(expected, got); !ok {
|
||||
t.Errorf("Payload does not match.\nActual: %+v\nExpected: %+v", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateToV7(t *testing.T) {
|
||||
ctx := testutils.InitCtx("../tmp")
|
||||
|
||||
// set up
|
||||
testutils.SetupTmp(ctx)
|
||||
testutils.WriteFile(ctx, "./fixtures/7-pre-actions.json", "actions")
|
||||
defer testutils.ClearTmp(ctx)
|
||||
|
||||
// execute
|
||||
if err := migrateToV7(ctx); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "migrating").Error())
|
||||
}
|
||||
|
||||
// test
|
||||
b := testutils.ReadFile(ctx, "actions")
|
||||
var got []migrateToV7Action
|
||||
if err := json.Unmarshal(b, &got); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling the result").Error())
|
||||
}
|
||||
|
||||
b2 := testutils.ReadFileAbs("./fixtures/7-post-actions.json")
|
||||
var expected []migrateToV7Action
|
||||
if err := json.Unmarshal(b, &expected); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "unmarshalling the result into Dnote").Error())
|
||||
}
|
||||
|
||||
ok, err := testutils.IsEqualJSON(b, b2)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "comparing JSON").Error())
|
||||
}
|
||||
|
||||
if !ok {
|
||||
t.Errorf("Result does not match.\nActual: %+v\nExpected: %+v", got, expected)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,383 +0,0 @@
|
|||
package migrate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/cli/infra"
|
||||
"github.com/dnote/cli/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/satori/go.uuid"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// migrateToV1 deletes YAML archive if exists
|
||||
func migrateToV1(ctx infra.DnoteCtx) error {
|
||||
yamlPath := fmt.Sprintf("%s/%s", ctx.HomeDir, ".dnote-yaml-archived")
|
||||
if !utils.FileExists(yamlPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.Remove(yamlPath); err != nil {
|
||||
return errors.Wrap(err, "Failed to delete .dnote archive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateToV2(ctx infra.DnoteCtx) error {
|
||||
notePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
|
||||
|
||||
b, err := ioutil.ReadFile(notePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the note file")
|
||||
}
|
||||
|
||||
var preDnote migrateToV2PreDnote
|
||||
postDnote := migrateToV2PostDnote{}
|
||||
|
||||
err = json.Unmarshal(b, &preDnote)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
|
||||
}
|
||||
|
||||
for bookName, book := range preDnote {
|
||||
var notes = make([]migrateToV2PostNote, 0, len(book))
|
||||
for _, note := range book {
|
||||
newNote := migrateToV2PostNote{
|
||||
UUID: uuid.NewV4().String(),
|
||||
Content: note.Content,
|
||||
AddedOn: note.AddedOn,
|
||||
EditedOn: 0,
|
||||
}
|
||||
|
||||
notes = append(notes, newNote)
|
||||
}
|
||||
|
||||
b := migrateToV2PostBook{
|
||||
Name: bookName,
|
||||
Notes: notes,
|
||||
}
|
||||
|
||||
postDnote[bookName] = b
|
||||
}
|
||||
|
||||
d, err := json.MarshalIndent(postDnote, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal new dnote into JSON")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(notePath, d, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write the new dnote into the file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateToV3 generates actions for existing dnote
|
||||
func migrateToV3(ctx infra.DnoteCtx) error {
|
||||
notePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
|
||||
actionsPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
|
||||
|
||||
b, err := ioutil.ReadFile(notePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the note file")
|
||||
}
|
||||
|
||||
var dnote migrateToV3Dnote
|
||||
|
||||
err = json.Unmarshal(b, &dnote)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
|
||||
}
|
||||
|
||||
var actions []migrateToV3Action
|
||||
|
||||
for bookName, book := range dnote {
|
||||
// Find the minimum added_on timestamp from the notes that belong to the book
|
||||
// to give timstamp to the add_book action.
|
||||
// Logically add_book must have happened no later than the first add_note
|
||||
// to the book in order for sync to work.
|
||||
minTs := time.Now().Unix()
|
||||
for _, note := range book.Notes {
|
||||
if note.AddedOn < minTs {
|
||||
minTs = note.AddedOn
|
||||
}
|
||||
}
|
||||
|
||||
action := migrateToV3Action{
|
||||
Type: migrateToV3ActionAddBook,
|
||||
Data: map[string]interface{}{
|
||||
"book_name": bookName,
|
||||
},
|
||||
Timestamp: minTs,
|
||||
}
|
||||
actions = append(actions, action)
|
||||
|
||||
for _, note := range book.Notes {
|
||||
action := migrateToV3Action{
|
||||
Type: migrateToV3ActionAddNote,
|
||||
Data: map[string]interface{}{
|
||||
"note_uuid": note.UUID,
|
||||
"book_name": book.Name,
|
||||
"content": note.Content,
|
||||
},
|
||||
Timestamp: note.AddedOn,
|
||||
}
|
||||
actions = append(actions, action)
|
||||
}
|
||||
}
|
||||
|
||||
a, err := json.Marshal(actions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal actions into JSON")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(actionsPath, a, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write the actions into a file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEditorCommand() string {
|
||||
editor := os.Getenv("EDITOR")
|
||||
|
||||
switch editor {
|
||||
case "atom":
|
||||
return "atom -w"
|
||||
case "subl":
|
||||
return "subl -n -w"
|
||||
case "mate":
|
||||
return "mate -w"
|
||||
case "vim":
|
||||
return "vim"
|
||||
case "nano":
|
||||
return "nano"
|
||||
case "emacs":
|
||||
return "emacs"
|
||||
default:
|
||||
return "vi"
|
||||
}
|
||||
}
|
||||
|
||||
func migrateToV4(ctx infra.DnoteCtx) error {
|
||||
configPath := fmt.Sprintf("%s/dnoterc", ctx.DnoteDir)
|
||||
|
||||
b, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the config file")
|
||||
}
|
||||
|
||||
var preConfig migrateToV4PreConfig
|
||||
err = yaml.Unmarshal(b, &preConfig)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to unmarshal existing config into JSON")
|
||||
}
|
||||
|
||||
postConfig := migrateToV4PostConfig{
|
||||
APIKey: preConfig.APIKey,
|
||||
Editor: getEditorCommand(),
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(postConfig)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal config into JSON")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(configPath, data, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write the config into a file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateToV5 migrates actions
|
||||
func migrateToV5(ctx infra.DnoteCtx) error {
|
||||
actionsPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
|
||||
|
||||
b, err := ioutil.ReadFile(actionsPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading the actions file")
|
||||
}
|
||||
|
||||
var actions []migrateToV5PreAction
|
||||
err = json.Unmarshal(b, &actions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarshalling actions to JSON")
|
||||
}
|
||||
|
||||
result := []migrateToV5PostAction{}
|
||||
|
||||
for _, action := range actions {
|
||||
var data json.RawMessage
|
||||
|
||||
switch action.Type {
|
||||
case migrateToV5ActionEditNote:
|
||||
var oldData migrateToV5PreEditNoteData
|
||||
if err = json.Unmarshal(action.Data, &oldData); err != nil {
|
||||
return errors.Wrapf(err, "unmarshalling old data of an edit note action %d", action.ID)
|
||||
}
|
||||
|
||||
migratedData := migrateToV5PostEditNoteData{
|
||||
NoteUUID: oldData.NoteUUID,
|
||||
FromBook: oldData.BookName,
|
||||
Content: oldData.Content,
|
||||
}
|
||||
b, err = json.Marshal(migratedData)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling data")
|
||||
}
|
||||
|
||||
data = b
|
||||
default:
|
||||
data = action.Data
|
||||
}
|
||||
|
||||
migrated := migrateToV5PostAction{
|
||||
UUID: uuid.NewV4().String(),
|
||||
Schema: 1,
|
||||
Type: action.Type,
|
||||
Data: data,
|
||||
Timestamp: action.Timestamp,
|
||||
}
|
||||
|
||||
result = append(result, migrated)
|
||||
}
|
||||
|
||||
a, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling result into JSON")
|
||||
}
|
||||
err = ioutil.WriteFile(actionsPath, a, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "writing the result into a file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateToV6 adds a 'public' field to notes
|
||||
func migrateToV6(ctx infra.DnoteCtx) error {
|
||||
notePath := fmt.Sprintf("%s/dnote", ctx.DnoteDir)
|
||||
|
||||
b, err := ioutil.ReadFile(notePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read the note file")
|
||||
}
|
||||
|
||||
var preDnote migrateToV6PreDnote
|
||||
postDnote := migrateToV6PostDnote{}
|
||||
|
||||
err = json.Unmarshal(b, &preDnote)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
|
||||
}
|
||||
|
||||
for bookName, book := range preDnote {
|
||||
var notes = make([]migrateToV6PostNote, 0, len(book.Notes))
|
||||
public := false
|
||||
for _, note := range book.Notes {
|
||||
newNote := migrateToV6PostNote{
|
||||
UUID: note.UUID,
|
||||
Content: note.Content,
|
||||
AddedOn: note.AddedOn,
|
||||
EditedOn: note.EditedOn,
|
||||
Public: &public,
|
||||
}
|
||||
|
||||
notes = append(notes, newNote)
|
||||
}
|
||||
|
||||
b := migrateToV6PostBook{
|
||||
Name: bookName,
|
||||
Notes: notes,
|
||||
}
|
||||
|
||||
postDnote[bookName] = b
|
||||
}
|
||||
|
||||
d, err := json.MarshalIndent(postDnote, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal new dnote into JSON")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(notePath, d, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to write the new dnote into the file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateToV7 migrates data of edit_note action to the proper version which is
|
||||
// EditNoteDataV2. Due to a bug, edit logged actions with schema version '2'
|
||||
// but with a data of EditNoteDataV1. https://github.com/dnote/cli/issues/107
|
||||
func migrateToV7(ctx infra.DnoteCtx) error {
|
||||
actionPath := fmt.Sprintf("%s/actions", ctx.DnoteDir)
|
||||
|
||||
b, err := ioutil.ReadFile(actionPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading actions file")
|
||||
}
|
||||
|
||||
var preActions []migrateToV7Action
|
||||
postActions := []migrateToV7Action{}
|
||||
err = json.Unmarshal(b, &preActions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarhsalling existing actions")
|
||||
}
|
||||
|
||||
for _, action := range preActions {
|
||||
var newAction migrateToV7Action
|
||||
|
||||
if action.Type == migrateToV7ActionTypeEditNote {
|
||||
var oldData migrateToV7EditNoteDataV1
|
||||
if e := json.Unmarshal(action.Data, &oldData); e != nil {
|
||||
return errors.Wrapf(e, "unmarshalling data of action with uuid %s", action.Data)
|
||||
}
|
||||
|
||||
newData := migrateToV7EditNoteDataV2{
|
||||
NoteUUID: oldData.NoteUUID,
|
||||
FromBook: oldData.FromBook,
|
||||
ToBook: nil,
|
||||
Content: &oldData.Content,
|
||||
Public: nil,
|
||||
}
|
||||
d, e := json.Marshal(newData)
|
||||
if e != nil {
|
||||
return errors.Wrapf(e, "marshalling new data of action with uuid %s", action.Data)
|
||||
}
|
||||
|
||||
newAction = migrateToV7Action{
|
||||
UUID: action.UUID,
|
||||
Schema: action.Schema,
|
||||
Type: action.Type,
|
||||
Timestamp: action.Timestamp,
|
||||
Data: d,
|
||||
}
|
||||
} else {
|
||||
newAction = action
|
||||
}
|
||||
|
||||
postActions = append(postActions, newAction)
|
||||
}
|
||||
|
||||
d, err := json.Marshal(postActions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling new actions")
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(actionPath, d, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "writing new actions to a file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
package migrate
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// v2
|
||||
type migrateToV2PreNote struct {
|
||||
UID string
|
||||
Content string
|
||||
AddedOn int64
|
||||
}
|
||||
type migrateToV2PostNote struct {
|
||||
UUID string `json:"uuid"`
|
||||
Content string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"editd_on"`
|
||||
}
|
||||
type migrateToV2PreBook []migrateToV2PreNote
|
||||
type migrateToV2PostBook struct {
|
||||
Name string `json:"name"`
|
||||
Notes []migrateToV2PostNote `json:"notes"`
|
||||
}
|
||||
type migrateToV2PreDnote map[string]migrateToV2PreBook
|
||||
type migrateToV2PostDnote map[string]migrateToV2PostBook
|
||||
|
||||
//v3
|
||||
var (
|
||||
migrateToV3ActionAddNote = "add_note"
|
||||
migrateToV3ActionAddBook = "add_book"
|
||||
)
|
||||
|
||||
type migrateToV3Note struct {
|
||||
UUID string `json:"uuid"`
|
||||
Content string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
}
|
||||
type migrateToV3Book struct {
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Notes []migrateToV3Note `json:"notes"`
|
||||
}
|
||||
type migrateToV3Dnote map[string]migrateToV3Book
|
||||
type migrateToV3Action struct {
|
||||
Type string `json:"type"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// v4
|
||||
type migrateToV4PreConfig struct {
|
||||
Book string
|
||||
APIKey string
|
||||
}
|
||||
type migrateToV4PostConfig struct {
|
||||
Editor string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
// v5
|
||||
type migrateToV5AddNoteData struct {
|
||||
NoteUUID string `json:"note_uuid"`
|
||||
BookName string `json:"book_name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
type migrateToV5RemoveNoteData struct {
|
||||
NoteUUID string `json:"note_uuid"`
|
||||
BookName string `json:"book_name"`
|
||||
}
|
||||
type migrateToV5AddBookData struct {
|
||||
BookName string `json:"book_name"`
|
||||
}
|
||||
type migrateToV5RemoveBookData struct {
|
||||
BookName string `json:"book_name"`
|
||||
}
|
||||
type migrateToV5PreEditNoteData struct {
|
||||
NoteUUID string `json:"note_uuid"`
|
||||
BookName string `json:"book_name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
type migrateToV5PostEditNoteData struct {
|
||||
NoteUUID string `json:"note_uuid"`
|
||||
FromBook string `json:"from_book"`
|
||||
ToBook string `json:"to_book"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
type migrateToV5PreAction struct {
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
type migrateToV5PostAction struct {
|
||||
UUID string `json:"uuid"`
|
||||
Schema int `json:"schema"`
|
||||
Type string `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
var (
|
||||
migrateToV5ActionAddNote = "add_note"
|
||||
migrateToV5ActionRemoveNote = "remove_note"
|
||||
migrateToV5ActionEditNote = "edit_note"
|
||||
migrateToV5ActionAddBook = "add_book"
|
||||
migrateToV5ActionRemoveBook = "remove_book"
|
||||
)
|
||||
|
||||
// v6
|
||||
type migrateToV6PreNote struct {
|
||||
UUID string `json:"uuid"`
|
||||
Content string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
}
|
||||
type migrateToV6PostNote struct {
|
||||
UUID string `json:"uuid"`
|
||||
Content string `json:"content"`
|
||||
AddedOn int64 `json:"added_on"`
|
||||
EditedOn int64 `json:"edited_on"`
|
||||
// Make a pointer to test absent values
|
||||
Public *bool `json:"public"`
|
||||
}
|
||||
type migrateToV6PreBook struct {
|
||||
Name string `json:"name"`
|
||||
Notes []migrateToV6PreNote `json:"notes"`
|
||||
}
|
||||
type migrateToV6PostBook struct {
|
||||
Name string `json:"name"`
|
||||
Notes []migrateToV6PostNote `json:"notes"`
|
||||
}
|
||||
type migrateToV6PreDnote map[string]migrateToV6PreBook
|
||||
type migrateToV6PostDnote map[string]migrateToV6PostBook
|
||||
|
||||
// v7
|
||||
var migrateToV7ActionTypeEditNote = "edit_note"
|
||||
|
||||
type migrateToV7Action struct {
|
||||
UUID string `json:"uuid"`
|
||||
Schema int `json:"schema"`
|
||||
Type string `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
type migrateToV7EditNoteDataV1 struct {
|
||||
NoteUUID string `json:"note_uuid"`
|
||||
FromBook string `json:"from_book"`
|
||||
ToBook string `json:"to_book"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
type migrateToV7EditNoteDataV2 struct {
|
||||
NoteUUID string `json:"note_uuid"`
|
||||
FromBook string `json:"from_book"`
|
||||
ToBook *string `json:"to_book"`
|
||||
Content *string `json:"content"`
|
||||
Public *bool `json:"public"`
|
||||
}
|
||||
1
migrate/sql.go
Normal file
1
migrate/sql.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package migrate
|
||||
|
|
@ -3,5 +3,5 @@
|
|||
# dev.sh builds a new binary and replaces the old one in the PATH with it
|
||||
|
||||
rm "$(which dnote)" $GOPATH/bin/cli
|
||||
make build-snapshot
|
||||
ln -s $GOPATH/src/github.com/dnote/cli/dist/darwin_amd64/dnote /usr/local/bin/dnote
|
||||
go install -ldflags "-X main.apiEndpoint=http://127.0.0.1:5000" --tags "darwin" .
|
||||
ln -s $GOPATH/bin/cli /usr/local/bin/dnote
|
||||
|
|
|
|||
5
scripts/dump_schema.sh
Executable file
5
scripts/dump_schema.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
# dump_schema.sh dumps the current system's dnote schema to testutils package
|
||||
# to be used while setting up tests
|
||||
|
||||
sqlite3 ~/.dnote/dnote.db .schema > ./testutils/fixtures/schema.sql
|
||||
|
|
@ -4,3 +4,4 @@
|
|||
# https://stackoverflow.com/questions/23715302/go-how-to-run-tests-for-multiple-packages
|
||||
|
||||
go test ./... -p 1
|
||||
|
||||
|
|
|
|||
32
testutils/fixtures/schema.sql
Normal file
32
testutils/fixtures/schema.sql
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
CREATE TABLE notes
|
||||
(
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
uuid text NOT NULL,
|
||||
book_uuid text NOT NULL,
|
||||
content text NOT NULL,
|
||||
added_on integer NOT NULL,
|
||||
edited_on integer DEFAULT 0,
|
||||
public bool DEFAULT false
|
||||
);
|
||||
CREATE TABLE books
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
label text NOT NULL
|
||||
);
|
||||
CREATE TABLE actions
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
schema integer NOT NULL,
|
||||
type text NOT NULL,
|
||||
data text NOT NULL,
|
||||
timestamp integer NOT NULL
|
||||
);
|
||||
CREATE TABLE system
|
||||
(
|
||||
key string NOT NULL,
|
||||
value text NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_books_label ON books(label);
|
||||
CREATE INDEX idx_books_uuid ON books(uuid);
|
||||
CREATE INDEX idx_notes_id ON notes(id);
|
||||
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);
|
||||
|
|
@ -2,12 +2,17 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/cli/infra"
|
||||
|
|
@ -15,21 +20,47 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func InitCtx(relPath string) infra.DnoteCtx {
|
||||
// InitEnv sets up a test env and returns a new dnote context
|
||||
func InitEnv(relPath string, relFixturePath string) infra.DnoteCtx {
|
||||
path, err := filepath.Abs(relPath)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "pasrsing path").Error())
|
||||
}
|
||||
|
||||
os.Setenv("DNOTE_HOME_DIR", path)
|
||||
ctx, err := infra.NewCtx("", "")
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "getting new ctx").Error())
|
||||
}
|
||||
|
||||
// set up directory and db
|
||||
if err := os.MkdirAll(ctx.DnoteDir, 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ctx := infra.DnoteCtx{
|
||||
HomeDir: path,
|
||||
DnoteDir: fmt.Sprintf("%s/.dnote", path),
|
||||
b := ReadFileAbs(relFixturePath)
|
||||
setupSQL := string(b)
|
||||
|
||||
db := ctx.DB
|
||||
_, err = db.Exec(setupSQL)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "running schema sql").Error())
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func WriteFile(ctx infra.DnoteCtx, fixturePath string, filename string) {
|
||||
// TeardownEnv cleans up the test env represented by the given context
|
||||
func TeardownEnv(ctx infra.DnoteCtx) {
|
||||
ctx.DB.Close()
|
||||
|
||||
if err := os.RemoveAll(ctx.DnoteDir); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// CopyFixture writes the content of the given fixture to the filename inside the dnote dir
|
||||
func CopyFixture(ctx infra.DnoteCtx, fixturePath string, filename string) {
|
||||
fp, err := filepath.Abs(fixturePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
@ -45,7 +76,8 @@ func WriteFile(ctx infra.DnoteCtx, fixturePath string, filename string) {
|
|||
}
|
||||
}
|
||||
|
||||
func WriteFileWithContent(ctx infra.DnoteCtx, content []byte, filename string) {
|
||||
// WriteFile writes a file with the given content and filename inside the dnote dir
|
||||
func WriteFile(ctx infra.DnoteCtx, content []byte, filename string) {
|
||||
dp, err := filepath.Abs(filepath.Join(ctx.DnoteDir, filename))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
@ -56,6 +88,7 @@ func WriteFileWithContent(ctx infra.DnoteCtx, content []byte, filename string) {
|
|||
}
|
||||
}
|
||||
|
||||
// ReadFile reads the content of the file with the given name in dnote dir
|
||||
func ReadFile(ctx infra.DnoteCtx, filename string) []byte {
|
||||
path := filepath.Join(ctx.DnoteDir, filename)
|
||||
|
||||
|
|
@ -67,8 +100,10 @@ func ReadFile(ctx infra.DnoteCtx, filename string) []byte {
|
|||
return b
|
||||
}
|
||||
|
||||
func ReadFileAbs(filename string) []byte {
|
||||
fp, err := filepath.Abs(filename)
|
||||
// ReadFileAbs reads the content of the file with the given file path by resolving
|
||||
// it as an absolute path
|
||||
func ReadFileAbs(relpath string) []byte {
|
||||
fp, err := filepath.Abs(relpath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
@ -81,27 +116,36 @@ func ReadFileAbs(filename string) []byte {
|
|||
return b
|
||||
}
|
||||
|
||||
func SetupTmp(ctx infra.DnoteCtx) {
|
||||
if err := os.MkdirAll(ctx.DnoteDir, 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ClearTmp(ctx infra.DnoteCtx) {
|
||||
if err := os.RemoveAll(ctx.DnoteDir); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertEqual fails a test if the actual does not match the expected
|
||||
func AssertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
||||
func checkEqual(a interface{}, b interface{}, message string) (bool, string) {
|
||||
if a == b {
|
||||
return
|
||||
return true, ""
|
||||
}
|
||||
|
||||
var m string
|
||||
if len(message) == 0 {
|
||||
message = fmt.Sprintf("%v != %v", a, b)
|
||||
m = fmt.Sprintf("%v != %v", a, b)
|
||||
} else {
|
||||
m = message
|
||||
}
|
||||
errorMessage := fmt.Sprintf("%s. Actual: %+v. Expected: %+v.", m, a, b)
|
||||
|
||||
return false, errorMessage
|
||||
}
|
||||
|
||||
// AssertEqual errors a test if the actual does not match the expected
|
||||
func AssertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
||||
ok, m := checkEqual(a, b, message)
|
||||
if !ok {
|
||||
t.Error(m)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertEqualf fails a test if the actual does not match the expected
|
||||
func AssertEqualf(t *testing.T, a interface{}, b interface{}, message string) {
|
||||
ok, m := checkEqual(a, b, message)
|
||||
if !ok {
|
||||
t.Fatal(m)
|
||||
}
|
||||
t.Errorf("%s. Actual: %+v. Expected: %+v.", message, a, b)
|
||||
}
|
||||
|
||||
// AssertNotEqual fails a test if the actual matches the expected
|
||||
|
|
@ -153,3 +197,109 @@ func IsEqualJSON(s1, s2 []byte) (bool, error) {
|
|||
|
||||
return reflect.DeepEqual(o1, o2), nil
|
||||
}
|
||||
|
||||
// MustExec executes the given SQL query and fails a test if an error occurs
|
||||
func MustExec(t *testing.T, message string, db *sql.DB, query string, args ...interface{}) sql.Result {
|
||||
result, err := db.Exec(query, args...)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(errors.Wrap(err, "executing sql"), message))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MustScan scans the given row and fails a test in case of any errors
|
||||
func MustScan(t *testing.T, message string, row *sql.Row, args ...interface{}) {
|
||||
err := row.Scan(args...)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(errors.Wrap(err, "scanning a row"), message))
|
||||
}
|
||||
}
|
||||
|
||||
// NewDnoteCmd returns a new Dnote command and a pointer to stderr
|
||||
func NewDnoteCmd(ctx infra.DnoteCtx, binaryName string, arg ...string) (*exec.Cmd, *bytes.Buffer, *bytes.Buffer, error) {
|
||||
var stderr, stdout bytes.Buffer
|
||||
|
||||
binaryPath, err := filepath.Abs(binaryName)
|
||||
if err != nil {
|
||||
return &exec.Cmd{}, &stderr, &stdout, errors.Wrap(err, "getting the absolute path to the test binary")
|
||||
}
|
||||
|
||||
cmd := exec.Command(binaryPath, arg...)
|
||||
cmd.Env = []string{fmt.Sprintf("DNOTE_DIR=%s", ctx.DnoteDir), fmt.Sprintf("DNOTE_HOME_DIR=%s", ctx.HomeDir)}
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdout = &stdout
|
||||
|
||||
return cmd, &stderr, &stdout, nil
|
||||
}
|
||||
|
||||
// RunDnoteCmd runs a dnote command
|
||||
func RunDnoteCmd(t *testing.T, ctx infra.DnoteCtx, binaryName string, arg ...string) {
|
||||
t.Logf("running: %s %s", binaryName, strings.Join(arg, " "))
|
||||
|
||||
cmd, stderr, stdout, err := NewDnoteCmd(ctx, binaryName, arg...)
|
||||
if err != nil {
|
||||
t.Logf("\n%s", stdout)
|
||||
t.Fatal(errors.Wrap(err, "getting command").Error())
|
||||
}
|
||||
|
||||
cmd.Env = append(cmd.Env, "DNOTE_DEBUG=1")
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Logf("\n%s", stdout)
|
||||
t.Fatal(errors.Wrapf(err, "running command %s", stderr.String()))
|
||||
}
|
||||
|
||||
// Print stdout if and only if test fails later
|
||||
t.Logf("\n%s", stdout)
|
||||
}
|
||||
|
||||
// WaitDnoteCmd runs a dnote command and waits until the command is exited
|
||||
func WaitDnoteCmd(t *testing.T, ctx infra.DnoteCtx, runFunc func(io.WriteCloser) error, binaryName string, arg ...string) {
|
||||
t.Logf("running: %s %s", binaryName, strings.Join(arg, " "))
|
||||
|
||||
cmd, stderr, stdout, err := NewDnoteCmd(ctx, binaryName, arg...)
|
||||
if err != nil {
|
||||
t.Logf("\n%s", stdout)
|
||||
t.Fatal(errors.Wrap(err, "getting command").Error())
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
t.Logf("\n%s", stdout)
|
||||
t.Fatal(errors.Wrap(err, "getting stdin %s"))
|
||||
}
|
||||
defer stdin.Close()
|
||||
|
||||
// Start the program
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
t.Logf("\n%s", stdout)
|
||||
t.Fatal(errors.Wrap(err, "starting command"))
|
||||
}
|
||||
|
||||
err = runFunc(stdin)
|
||||
if err != nil {
|
||||
t.Logf("\n%s", stdout)
|
||||
t.Fatal(errors.Wrap(err, "running with stdin"))
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
t.Logf("\n%s", stdout)
|
||||
t.Fatal(errors.Wrapf(err, "running command %s", stderr.String()))
|
||||
}
|
||||
|
||||
// Print stdout if and only if test fails later
|
||||
t.Logf("\n%s", stdout)
|
||||
}
|
||||
|
||||
// UserConfirm simulates confirmation from the user by writing to stdin
|
||||
func UserConfirm(stdin io.WriteCloser) error {
|
||||
// confirm
|
||||
if _, err := io.WriteString(stdin, "y\n"); err != nil {
|
||||
return errors.Wrap(err, "confirming deletion")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
61
testutils/setup.go
Normal file
61
testutils/setup.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"github.com/dnote/cli/infra"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Setup1 sets up a dnote env #1
|
||||
// dnote4.json
|
||||
func Setup1(t *testing.T, ctx infra.DnoteCtx) {
|
||||
db := ctx.DB
|
||||
|
||||
b1UUID := "js-book-uuid"
|
||||
b2UUID := "linux-book-uuid"
|
||||
|
||||
MustExec(t, "setting up book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "js")
|
||||
MustExec(t, "setting up book 2", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "linux")
|
||||
|
||||
MustExec(t, "setting up note 1", db, "INSERT INTO notes (uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?)", "43827b9a-c2b0-4c06-a290-97991c896653", b1UUID, "Booleans have toString()", 1515199943)
|
||||
}
|
||||
|
||||
// Setup2 sets up a dnote env #2
|
||||
// dnote3.json
|
||||
func Setup2(t *testing.T, ctx infra.DnoteCtx) {
|
||||
db := ctx.DB
|
||||
|
||||
b1UUID := "js-book-uuid"
|
||||
b2UUID := "linux-book-uuid"
|
||||
|
||||
MustExec(t, "setting up book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "js")
|
||||
MustExec(t, "setting up book 2", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "linux")
|
||||
|
||||
MustExec(t, "setting up note 2", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 1, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", b1UUID, "Date object implements mathematical comparisons", 1515199951)
|
||||
MustExec(t, "setting up note 1", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 2, "43827b9a-c2b0-4c06-a290-97991c896653", b1UUID, "Booleans have toString()", 1515199943)
|
||||
MustExec(t, "setting up note 3", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 3, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", b2UUID, "wc -l to count words", 1515199961)
|
||||
}
|
||||
|
||||
// Setup3 sets up a dnote env #1
|
||||
// dnote1.json
|
||||
func Setup3(t *testing.T, ctx infra.DnoteCtx) {
|
||||
db := ctx.DB
|
||||
|
||||
b1UUID := "js-book-uuid"
|
||||
|
||||
MustExec(t, "setting up book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "js")
|
||||
|
||||
MustExec(t, "setting up note 1", db, "INSERT INTO notes (uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?)", "43827b9a-c2b0-4c06-a290-97991c896653", b1UUID, "Booleans have toString()", 1515199943)
|
||||
}
|
||||
|
||||
// Setup4 sets up a dnote env #1
|
||||
// dnote2.json
|
||||
func Setup4(t *testing.T, ctx infra.DnoteCtx) {
|
||||
db := ctx.DB
|
||||
|
||||
b1UUID := "js-book-uuid"
|
||||
|
||||
MustExec(t, "setting up book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "js")
|
||||
|
||||
MustExec(t, "setting up note 1", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 1, "43827b9a-c2b0-4c06-a290-97991c896653", b1UUID, "Booleans have toString()", 1515199943)
|
||||
MustExec(t, "setting up note 2", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 2, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", b1UUID, "Date object implements mathematical comparisons", 1515199951)
|
||||
}
|
||||
|
|
@ -4,35 +4,30 @@ import (
|
|||
"bufio"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/cli/log"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// GenerateUID returns a uid
|
||||
func GenerateUID() string {
|
||||
// GenerateUUID returns a uid
|
||||
func GenerateUUID() string {
|
||||
return uuid.NewV4().String()
|
||||
}
|
||||
|
||||
func GetInput() (string, error) {
|
||||
func getInput() (string, error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Failed to read stdin")
|
||||
return "", errors.Wrap(err, "reading stdin")
|
||||
}
|
||||
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// AskConfirmation prompts for user input to confirm a choice
|
||||
func AskConfirmation(question string, optimistic bool) (bool, error) {
|
||||
var choices string
|
||||
if optimistic {
|
||||
|
|
@ -43,7 +38,7 @@ func AskConfirmation(question string, optimistic bool) (bool, error) {
|
|||
|
||||
log.Printf("%s %s: ", question, choices)
|
||||
|
||||
res, err := GetInput()
|
||||
res, err := getInput()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "Failed to get user input")
|
||||
}
|
||||
|
|
@ -63,52 +58,39 @@ func FileExists(filepath string) bool {
|
|||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func IsDir(path string) (bool, error) {
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.Wrapf(err, "Failed to check if '%s' is directory", path)
|
||||
}
|
||||
|
||||
return fileInfo.IsDir(), nil
|
||||
}
|
||||
|
||||
// CopyFile copies a file from the src to dest
|
||||
func CopyFile(src, dest string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to open the input file")
|
||||
return errors.Wrap(err, "opening the input file")
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create the output file")
|
||||
return errors.Wrap(err, "creating the output file")
|
||||
}
|
||||
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
return errors.Wrap(err, "Failed to copy the file content")
|
||||
return errors.Wrap(err, "copying the file content")
|
||||
}
|
||||
|
||||
if err = out.Sync(); err != nil {
|
||||
return errors.Wrap(err, "Failed to flush the output file to disk")
|
||||
return errors.Wrap(err, "flushing the output file to disk")
|
||||
}
|
||||
|
||||
fi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get file info for the input file")
|
||||
return errors.Wrap(err, "getting the file info for the input file")
|
||||
}
|
||||
|
||||
if err = os.Chmod(dest, fi.Mode()); err != nil {
|
||||
return errors.Wrap(err, "Failed to copy permission to the output file")
|
||||
return errors.Wrap(err, "copying permission to the output file")
|
||||
}
|
||||
|
||||
// Close the output file
|
||||
if err = out.Close(); err != nil {
|
||||
return errors.Wrap(err, "Failed to close the output file")
|
||||
return errors.Wrap(err, "closing the output file")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -122,25 +104,25 @@ func CopyDir(src, dest string) error {
|
|||
|
||||
fi, err := os.Stat(srcPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to get file info for the input")
|
||||
return errors.Wrap(err, "getting the file info for the input")
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return errors.Wrap(err, "Source is not a directory")
|
||||
return errors.New("source is not a directory")
|
||||
}
|
||||
|
||||
_, err = os.Stat(dest)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return errors.Wrap(err, "Failed to look up the destination")
|
||||
return errors.Wrap(err, "looking up the destination")
|
||||
}
|
||||
|
||||
err = os.MkdirAll(dest, fi.Mode())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create destination")
|
||||
return errors.Wrap(err, "creating destination")
|
||||
}
|
||||
|
||||
entries, err := ioutil.ReadDir(src)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to read directory listing for the input")
|
||||
return errors.Wrap(err, "reading the directory listing for the input")
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
|
|
@ -149,11 +131,11 @@ func CopyDir(src, dest string) error {
|
|||
|
||||
if entry.IsDir() {
|
||||
if err = CopyDir(srcEntryPath, destEntryPath); err != nil {
|
||||
return errors.Wrapf(err, "Failed to copy %s", entry.Name())
|
||||
return errors.Wrapf(err, "copying %s", entry.Name())
|
||||
}
|
||||
} else {
|
||||
if err = CopyFile(srcEntryPath, destEntryPath); err != nil {
|
||||
return errors.Wrapf(err, "Failed to copy %s", entry.Name())
|
||||
return errors.Wrapf(err, "copying %s", entry.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue