diff --git a/pkg/cli/.gitignore b/pkg/cli/.gitignore index 7de9e987..87f89ac4 100644 --- a/pkg/cli/.gitignore +++ b/pkg/cli/.gitignore @@ -1,6 +1,6 @@ main *.swo -tmp/ +tmp*/ /vendor test-dnote /dist diff --git a/pkg/cli/cmd/add/add.go b/pkg/cli/cmd/add/add.go index 11be9ec0..ab237267 100644 --- a/pkg/cli/cmd/add/add.go +++ b/pkg/cli/cmd/add/add.go @@ -115,8 +115,12 @@ func newRun(ctx context.DnoteCtx) infra.RunEFunc { } if content == "" { - fpath := ui.GetTmpContentPath(ctx) - err := ui.GetEditorInput(ctx, fpath, &content) + fpath, err := ui.GetTmpContentPath(ctx) + if err != nil { + return errors.Wrap(err, "getting temporarily content file path") + } + + err = ui.GetEditorInput(ctx, fpath, &content) if err != nil { return errors.Wrap(err, "Failed to get editor input") } diff --git a/pkg/cli/cmd/edit/edit.go b/pkg/cli/cmd/edit/edit.go index 4ec1daa9..cdc4210a 100644 --- a/pkg/cli/cmd/edit/edit.go +++ b/pkg/cli/cmd/edit/edit.go @@ -89,7 +89,10 @@ func newRun(ctx context.DnoteCtx) infra.RunEFunc { } if newContent == "" { - fpath := ui.GetTmpContentPath(ctx) + fpath, err := ui.GetTmpContentPath(ctx) + if err != nil { + return errors.Wrap(err, "getting temporarily content file path") + } e := ioutil.WriteFile(fpath, []byte(oldContent), 0644) if e != nil { diff --git a/pkg/cli/consts/consts.go b/pkg/cli/consts/consts.go index 3212dfcd..46f196da 100644 --- a/pkg/cli/consts/consts.go +++ b/pkg/cli/consts/consts.go @@ -24,8 +24,10 @@ var ( DnoteDirName = ".dnote" // DnoteDBFileName is a filename for the Dnote SQLite database DnoteDBFileName = "dnote.db" - // TmpContentFilename is the filename for a temporary content - TmpContentFilename = "DNOTE_TMPCONTENT.md" + // TmpContentFileBase is the base for the filename for a temporary content + TmpContentFileBase = "DNOTE_TMPCONTENT" + // TmpContentFileExt is the extension for the temporary content file + TmpContentFileExt = "md" // ConfigFilename is the name of the config file ConfigFilename = "dnoterc" diff --git a/pkg/cli/infra/init.go b/pkg/cli/infra/init.go index 6b278eb7..f038ec05 100644 --- a/pkg/cli/infra/init.go +++ b/pkg/cli/infra/init.go @@ -315,7 +315,11 @@ func getEditorCommand() string { func initDnoteDir(ctx context.DnoteCtx) error { path := ctx.DnoteDir - if utils.FileExists(path) { + ok, err := utils.FileExists(path) + if err != nil { + return errors.Wrap(err, "checking if dnote dir exists") + } + if ok { return nil } @@ -329,7 +333,11 @@ func initDnoteDir(ctx context.DnoteCtx) error { // initConfigFile populates a new config file if it does not exist yet func initConfigFile(ctx context.DnoteCtx, apiEndpoint string) error { path := config.GetPath(ctx) - if utils.FileExists(path) { + ok, err := utils.FileExists(path) + if err != nil { + return errors.Wrap(err, "checking if config exists") + } + if ok { return nil } diff --git a/pkg/cli/main_test.go b/pkg/cli/main_test.go index ae6e7aab..8bfc339d 100644 --- a/pkg/cli/main_test.go +++ b/pkg/cli/main_test.go @@ -57,10 +57,19 @@ func TestInit(t *testing.T) { db := database.OpenTestDB(t, opts.DnoteDir) // Test - if !utils.FileExists(opts.DnoteDir) { + ok, err := utils.FileExists(opts.DnoteDir) + if err != nil { + t.Fatal(errors.Wrap(err, "checking if dnote dir exists")) + } + if !ok { t.Errorf("dnote directory was not initialized") } - if !utils.FileExists(fmt.Sprintf("%s/%s", opts.DnoteDir, consts.ConfigFilename)) { + + ok, err = utils.FileExists(fmt.Sprintf("%s/%s", opts.DnoteDir, consts.ConfigFilename)) + if err != nil { + t.Fatal(errors.Wrap(err, "checking if dnote config exists")) + } + if !ok { t.Errorf("config file was not initialized") } diff --git a/pkg/cli/migrate/legacy.go b/pkg/cli/migrate/legacy.go index ea537caa..e9105cc4 100644 --- a/pkg/cli/migrate/legacy.go +++ b/pkg/cli/migrate/legacy.go @@ -85,7 +85,11 @@ func makeSchema(complete bool) schema { func Legacy(ctx context.DnoteCtx) error { // If schema does not exist, no need run a legacy migration schemaPath := getSchemaPath(ctx) - if ok := utils.FileExists(schemaPath); !ok { + ok, err := utils.FileExists(schemaPath) + if err != nil { + return errors.Wrap(err, "checking if schema exists") + } + if !ok { return nil } @@ -472,7 +476,11 @@ var migrateToV8SystemKeyBookMark = "bookmark" // migrateToV1 deletes YAML archive if exists func migrateToV1(ctx context.DnoteCtx) error { yamlPath := fmt.Sprintf("%s/%s", ctx.HomeDir, ".dnote-yaml-archived") - if !utils.FileExists(yamlPath) { + ok, err := utils.FileExists(yamlPath) + if err != nil { + return errors.Wrap(err, "checking if yaml file exists") + } + if !ok { return nil } diff --git a/pkg/cli/migrate/legacy_test.go b/pkg/cli/migrate/legacy_test.go index 012af095..fcda3399 100644 --- a/pkg/cli/migrate/legacy_test.go +++ b/pkg/cli/migrate/legacy_test.go @@ -72,7 +72,11 @@ func TestMigrateToV1(t *testing.T) { } // test - if utils.FileExists(yamlPath) { + ok, err := utils.FileExists(yamlPath) + if err != nil { + t.Fatal(errors.Wrap(err, "checking if yaml file exists")) + } + if ok { t.Fatal("YAML archive file has not been deleted") } }) @@ -93,7 +97,11 @@ func TestMigrateToV1(t *testing.T) { } // test - if utils.FileExists(yamlPath) { + ok, err := utils.FileExists(yamlPath) + if err != nil { + t.Fatal(errors.Wrap(err, "checking if yaml file exists")) + } + if ok { t.Fatal("YAML archive file must not exist") } }) @@ -372,23 +380,43 @@ func TestMigrateToV8(t *testing.T) { 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 { + + ok, err := utils.FileExists(dnoteFilePath) + if err != nil { + t.Fatal(errors.Wrap(err, "checking if file exists")) + } + if ok { t.Errorf("%s still exists", dnoteFilePath) } - if ok := utils.FileExists(schemaFilePath); ok { - t.Errorf("%s still exists", dnoteFilePath) + + ok, err = utils.FileExists(schemaFilePath) + if err != nil { + t.Fatal(errors.Wrap(err, "checking if file exists")) } - if ok := utils.FileExists(timestampFilePath); ok { - t.Errorf("%s still exists", dnoteFilePath) + if ok { + t.Errorf("%s still exists", schemaFilePath) } - if ok := utils.FileExists(dnotercPath); !ok { - t.Errorf("%s should exist", dnotercPath) + + ok, err = utils.FileExists(timestampFilePath) + if err != nil { + t.Fatal(errors.Wrap(err, "checking if file exists")) + } + if ok { + t.Errorf("%s still exists", timestampFilePath) + } + + ok, err = utils.FileExists(dnotercPath) + if err != nil { + t.Fatal(errors.Wrap(err, "checking if file exists")) + } + if !ok { + t.Errorf("%s still exists", dnotercPath) } // 2. test if notes and books are migrated var bookCount, noteCount int - err := db.QueryRow("SELECT count(*) FROM books").Scan(&bookCount) + err = db.QueryRow("SELECT count(*) FROM books").Scan(&bookCount) if err != nil { panic(errors.Wrap(err, "counting books")) } diff --git a/pkg/cli/ui/editor.go b/pkg/cli/ui/editor.go index bdee122c..5c0e1643 100644 --- a/pkg/cli/ui/editor.go +++ b/pkg/cli/ui/editor.go @@ -34,8 +34,19 @@ import ( // GetTmpContentPath returns the path to the temporary file containing // content being added or edited -func GetTmpContentPath(ctx context.DnoteCtx) string { - return fmt.Sprintf("%s/%s", ctx.DnoteDir, consts.TmpContentFilename) +func GetTmpContentPath(ctx context.DnoteCtx) (string, error) { + for i := 0; ; i++ { + filename := fmt.Sprintf("%s_%d.%s", consts.TmpContentFileBase, i, consts.TmpContentFileExt) + candidate := fmt.Sprintf("%s/%s", ctx.DnoteDir, filename) + + ok, err := utils.FileExists(candidate) + if err != nil { + return "", errors.Wrapf(err, "checking if file exists at %s", candidate) + } + if !ok { + return candidate, nil + } + } } // getEditorCommand returns the system's editor command with appropriate flags, @@ -91,7 +102,11 @@ func newEditorCmd(ctx context.DnoteCtx, fpath string) (*exec.Cmd, error) { // GetEditorInput gets the user input by launching a text editor and waiting for // it to exit func GetEditorInput(ctx context.DnoteCtx, fpath string, content *string) error { - if !utils.FileExists(fpath) { + ok, err := utils.FileExists(fpath) + if err != nil { + return errors.Wrapf(err, "checking if the file exists at %s", fpath) + } + if !ok { f, err := os.Create(fpath) if err != nil { return errors.Wrap(err, "creating a temporary content file") diff --git a/pkg/cli/ui/editor_test.go b/pkg/cli/ui/editor_test.go new file mode 100644 index 00000000..9fcdb4a9 --- /dev/null +++ b/pkg/cli/ui/editor_test.go @@ -0,0 +1,90 @@ +/* Copyright (C) 2019 Monomax Software Pty Ltd + * + * This file is part of Dnote CLI. + * + * Dnote CLI is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Dnote CLI is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Dnote CLI. If not, see . + */ + +package ui + +import ( + "fmt" + "os" + "testing" + + "github.com/dnote/dnote/pkg/assert" + "github.com/dnote/dnote/pkg/cli/context" + "github.com/pkg/errors" +) + +func TestGetTmpContentPath(t *testing.T) { + t.Run("no collision", func(t *testing.T) { + ctx := context.InitTestCtx(t, "../tmp1", nil) + defer context.TeardownTestCtx(t, ctx) + + res, err := GetTmpContentPath(ctx) + if err != nil { + t.Fatal(errors.Wrap(err, "executing")) + } + + expected := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_0.md") + assert.Equal(t, res, expected, "filename did not match") + }) + + t.Run("one existing session", func(t *testing.T) { + // set up + ctx := context.InitTestCtx(t, "../tmp2", nil) + defer context.TeardownTestCtx(t, ctx) + + p := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_0.md") + if _, err := os.Create(p); err != nil { + t.Fatal(errors.Wrap(err, "preparing the conflicting file")) + } + + // execute + res, err := GetTmpContentPath(ctx) + if err != nil { + t.Fatal(errors.Wrap(err, "executing")) + } + + // test + expected := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_1.md") + assert.Equal(t, res, expected, "filename did not match") + }) + + t.Run("two existing sessions", func(t *testing.T) { + // set up + ctx := context.InitTestCtx(t, "../tmp3", nil) + defer context.TeardownTestCtx(t, ctx) + + p1 := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_0.md") + if _, err := os.Create(p1); err != nil { + t.Fatal(errors.Wrap(err, "preparing the conflicting file")) + } + p2 := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_1.md") + if _, err := os.Create(p2); err != nil { + t.Fatal(errors.Wrap(err, "preparing the conflicting file")) + } + + // execute + res, err := GetTmpContentPath(ctx) + if err != nil { + t.Fatal(errors.Wrap(err, "executing")) + } + + // test + expected := fmt.Sprintf("%s/%s", ctx.DnoteDir, "DNOTE_TMPCONTENT_2.md") + assert.Equal(t, res, expected, "filename did not match") + }) +} diff --git a/pkg/cli/utils/files.go b/pkg/cli/utils/files.go index 452ee475..b5e2c501 100644 --- a/pkg/cli/utils/files.go +++ b/pkg/cli/utils/files.go @@ -44,9 +44,16 @@ func ReadFileAbs(relpath string) []byte { } // FileExists checks if the file exists at the given path -func FileExists(filepath string) bool { +func FileExists(filepath string) (bool, error) { _, err := os.Stat(filepath) - return !os.IsNotExist(err) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + + return false, errors.Wrap(err, "getting file info") } // CopyDir copies a directory from src to dest, recursively copying nested diff --git a/pkg/server/database/migrate/main.go b/pkg/server/database/migrate/main.go index ce132687..cf1207d5 100644 --- a/pkg/server/database/migrate/main.go +++ b/pkg/server/database/migrate/main.go @@ -1,3 +1,21 @@ +/* Copyright (C) 2019 Monomax Software Pty Ltd + * + * This file is part of Dnote. + * + * Dnote is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Dnote is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Dnote. If not, see . + */ + package main import (